From 143182fe2b3a6318eb03d19775064608dedc325f Mon Sep 17 00:00:00 2001 From: Sujesh T Date: Sat, 30 Nov 2019 16:41:14 +0530 Subject: [PATCH] feat: change hasone relation error message The current hasone error message is not appropriate one. So adds a better message. --- lib/model.js | 374 +++++--- test/relations.integration.js | 1611 ++++++++++++++++++--------------- 2 files changed, 1102 insertions(+), 883 deletions(-) diff --git a/lib/model.js b/lib/model.js index 20c3aed2..4f9d180c 100644 --- a/lib/model.js +++ b/lib/model.js @@ -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); } }; diff --git a/test/relations.integration.js b/test/relations.integration.js index 234e0a5f..95205481 100644 --- a/test/relations.integration.js +++ b/test/relations.integration.js @@ -20,11 +20,14 @@ describe('relations - integration', function() { lt.beforeEach.givenModel('store'); beforeEach(function(done) { this.widgetName = 'foo'; - this.store.widgets.create({ - name: this.widgetName, - }, function() { - done(); - }); + this.store.widgets.create( + { + name: this.widgetName, + }, + function() { + done(); + }, + ); }); afterEach(function(done) { this.app.models.widget.destroyAll(done); @@ -34,45 +37,51 @@ describe('relations - integration', function() { before(function defineProductAndCategoryModels() { const Team = app.registry.createModel('Team', {name: 'string'}); const Reader = app.registry.createModel('Reader', {name: 'string'}); - const Picture = app.registry.createModel('Picture', - {name: 'string', imageableId: 'number', imageableType: 'string'}); + const Picture = app.registry.createModel('Picture', { + name: 'string', + imageableId: 'number', + imageableType: 'string', + }); app.model(Team, {dataSource: 'db'}); app.model(Reader, {dataSource: 'db'}); app.model(Picture, {dataSource: 'db'}); - Reader.hasMany(Picture, {polymorphic: { // alternative syntax - as: 'imageable', // if not set, default to: reference - foreignKey: 'imageableId', // defaults to 'as + Id' - discriminator: 'imageableType', // defaults to 'as + Type' - }}); + Reader.hasMany(Picture, { + polymorphic: { + // alternative syntax + as: 'imageable', // if not set, default to: reference + foreignKey: 'imageableId', // defaults to 'as + Id' + discriminator: 'imageableType', // defaults to 'as + Type' + }, + }); - Picture.belongsTo('imageable', {polymorphic: { - foreignKey: 'imageableId', - discriminator: 'imageableType', - }}); + Picture.belongsTo('imageable', { + polymorphic: { + foreignKey: 'imageableId', + discriminator: 'imageableType', + }, + }); Reader.belongsTo(Team); }); before(function createEvent(done) { const test = this; - app.models.Team.create({name: 'Team 1'}, - function(err, team) { + app.models.Team.create({name: 'Team 1'}, function(err, team) { + if (err) return done(err); + + test.team = team; + app.models.Reader.create({name: 'Reader 1'}, function(err, reader) { if (err) return done(err); - test.team = team; - app.models.Reader.create({name: 'Reader 1'}, - function(err, reader) { - if (err) return done(err); - - test.reader = reader; - reader.pictures.create({name: 'Picture 1'}); - reader.pictures.create({name: 'Picture 2'}); - reader.team(test.team); - reader.save(done); - }); + test.reader = reader; + reader.pictures.create({name: 'Picture 1'}); + reader.pictures.create({name: 'Picture 2'}); + reader.team(test.team); + reader.save(done); }); + }); }); after(function(done) { @@ -82,14 +91,24 @@ describe('relations - integration', function() { it('includes the related child model', function(done) { const url = '/api/readers/' + this.reader.id; this.get(url) - .query({'filter': {'include': 'pictures'}}) + .query({filter: {include: 'pictures'}}) .expect(200, function(err, res) { if (err) return done(err); expect(res.body.name).to.be.equal('Reader 1'); expect(res.body.pictures).to.be.eql([ - {name: 'Picture 1', id: 1, imageableId: 1, imageableType: 'Reader'}, - {name: 'Picture 2', id: 2, imageableId: 1, imageableType: 'Reader'}, + { + name: 'Picture 1', + id: 1, + imageableId: 1, + imageableType: 'Reader', + }, + { + name: 'Picture 2', + id: 2, + imageableId: 1, + imageableType: 'Reader', + }, ]); done(); @@ -99,13 +118,17 @@ describe('relations - integration', function() { it('includes the related parent model', function(done) { const url = '/api/pictures'; this.get(url) - .query({'filter': {'include': 'imageable'}}) + .query({filter: {include: 'imageable'}}) .expect(200, function(err, res) { if (err) return done(err); expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); - expect(res.body[0].imageable).to.be.eql({name: 'Reader 1', id: 1, teamId: 1}); + expect(res.body[0].imageable).to.be.eql({ + name: 'Reader 1', + id: 1, + teamId: 1, + }); done(); }); @@ -114,17 +137,24 @@ describe('relations - integration', function() { it('includes related models scoped to the related parent model', function(done) { const url = '/api/pictures'; this.get(url) - .query({'filter': {'include': { - 'relation': 'imageable', - 'scope': {'include': 'team'}, - }}}) + .query({ + filter: { + include: { + relation: 'imageable', + scope: {include: 'team'}, + }, + }, + }) .expect(200, function(err, res) { if (err) return done(err); expect(res.body[0].name).to.be.equal('Picture 1'); expect(res.body[1].name).to.be.equal('Picture 2'); expect(res.body[0].imageable.name).to.be.eql('Reader 1'); - expect(res.body[0].imageable.team).to.be.eql({name: 'Team 1', id: 1}); + expect(res.body[0].imageable.team).to.be.eql({ + name: 'Team 1', + id: 1, + }); done(); }); @@ -133,14 +163,13 @@ describe('relations - integration', function() { describe('/store/superStores', function() { it('should invoke scoped methods remotely', function(done) { - this.get('/api/stores/superStores') - .expect(200, function(err, res) { - if (err) return done(err); + this.get('/api/stores/superStores').expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.an('array'); + expect(res.body).to.be.an('array'); - done(); - }); + done(); + }); }); }); @@ -148,34 +177,40 @@ describe('relations - integration', function() { beforeEach(function() { this.url = '/api/stores/' + this.store.id + '/widgets'; }); - lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - }); - describe('widgets (response.body)', function() { - beforeEach(function() { - debug('GET /api/stores/:id/widgets response: %s' + - '\nheaders: %j\nbody string: %s', - this.res.statusCode, - this.res.headers, - this.res.text); - this.widgets = this.res.body; - this.widget = this.res.body && this.res.body[0]; + lt.describe.whenCalledRemotely( + 'GET', + '/api/stores/:id/widgets', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); }); - it('should be an array', function() { - assert(Array.isArray(this.widgets)); + describe('widgets (response.body)', function() { + beforeEach(function() { + debug( + 'GET /api/stores/:id/widgets response: %s' + + '\nheaders: %j\nbody string: %s', + this.res.statusCode, + this.res.headers, + this.res.text, + ); + this.widgets = this.res.body; + this.widget = this.res.body && this.res.body[0]; + }); + it('should be an array', function() { + assert(Array.isArray(this.widgets)); + }); + it('should include a single widget', function() { + assert(this.widgets.length === 1); + assert(this.widget); + }); + it('should be a valid widget', function() { + assert(this.widget.id); + assert.equal(this.widget.storeId, this.store.id); + assert.equal(this.widget.name, this.widgetName); + }); }); - it('should include a single widget', function() { - assert(this.widgets.length === 1); - assert(this.widget); - }); - it('should be a valid widget', function() { - assert(this.widget.id); - assert.equal(this.widget.storeId, this.store.id); - assert.equal(this.widget.name, this.widgetName); - }); - }); - }); + }, + ); describe('POST /api/store/:id/widgets', function() { beforeEach(function() { this.newWidgetName = 'baz'; @@ -186,14 +221,16 @@ describe('relations - integration', function() { beforeEach(function(done) { this.http = this.post(this.url, this.newWidget); this.http.send(this.newWidget); - this.http.end(function(err) { - if (err) return done(err); + this.http.end( + function(err) { + if (err) return done(err); - this.req = this.http.req; - this.res = this.http.response; + this.req = this.http.req; + this.res = this.http.response; - done(); - }.bind(this)); + done(); + }.bind(this), + ); }); it('should succeed with statusCode 200', function() { assert.equal(this.res.statusCode, 200); @@ -213,32 +250,39 @@ describe('relations - integration', function() { }); }); it('should have a single widget with storeId', function(done) { - this.app.models.widget.count({ - storeId: this.store.id, - }, function(err, count) { - if (err) return done(err); + this.app.models.widget.count( + { + storeId: this.store.id, + }, + function(err, count) { + if (err) return done(err); - assert.equal(count, 2); + assert.equal(count, 2); - done(); - }); + done(); + }, + ); }); }); describe('PUT /api/store/:id/widgets/:fk', function() { beforeEach(function(done) { const self = this; - this.store.widgets.create({ - name: this.widgetName, - }, function(err, widget) { - self.widget = widget; - self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; - done(); - }); + this.store.widgets.create( + { + name: this.widgetName, + }, + function(err, widget) { + self.widget = widget; + self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; + done(); + }, + ); }); it('does not add default properties to request body', function(done) { const self = this; - self.request.put(self.url) + self.request + .put(self.url) .send({active: true}) .end(function(err) { if (err) return done(err); @@ -255,88 +299,116 @@ describe('relations - integration', function() { describe('/stores/:id/widgets/:fk - 200', function() { beforeEach(function(done) { const self = this; - this.store.widgets.create({ - name: this.widgetName, - }, function(err, widget) { - self.widget = widget; - self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; + this.store.widgets.create( + { + name: this.widgetName, + }, + function(err, widget) { + self.widget = widget; + self.url = '/api/stores/' + self.store.id + '/widgets/' + widget.id; - done(); - }); - }); - lt.describe.whenCalledRemotely('GET', '/stores/:id/widgets/:fk', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - assert.equal(this.res.body.id, this.widget.id); - }); + done(); + }, + ); }); + lt.describe.whenCalledRemotely( + 'GET', + '/stores/:id/widgets/:fk', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.id, this.widget.id); + }); + }, + ); }); describe('/stores/:id/widgets/:fk - 404', function() { beforeEach(function() { this.url = '/api/stores/' + this.store.id + '/widgets/123456'; }); - lt.describe.whenCalledRemotely('GET', '/stores/:id/widgets/:fk', function() { - it('should fail with statusCode 404', function() { - assert.equal(this.res.statusCode, 404); - assert.equal(this.res.body.error.statusCode, 404); - }); - }); + lt.describe.whenCalledRemotely( + 'GET', + '/stores/:id/widgets/:fk', + function() { + it('should fail with statusCode 404', function() { + assert.equal(this.res.statusCode, 404); + assert.equal(this.res.body.error.statusCode, 404); + }); + }, + ); }); describe('/store/:id/widgets/count', function() { beforeEach(function() { this.url = '/api/stores/' + this.store.id + '/widgets/count'; }); - lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets/count', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - }); - it('should return the count', function() { - assert.equal(this.res.body.count, 1); - }); - }); + lt.describe.whenCalledRemotely( + 'GET', + '/api/stores/:id/widgets/count', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + }); + it('should return the count', function() { + assert.equal(this.res.body.count, 1); + }); + }, + ); }); describe('/store/:id/widgets/count - filtered (matches)', function() { beforeEach(function() { - this.url = '/api/stores/' + this.store.id + '/widgets/count?where[name]=foo'; - }); - lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets/count?where[name]=foo', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - }); - it('should return the count', function() { - assert.equal(this.res.body.count, 1); - }); + this.url = + '/api/stores/' + this.store.id + '/widgets/count?where[name]=foo'; }); + lt.describe.whenCalledRemotely( + 'GET', + '/api/stores/:id/widgets/count?where[name]=foo', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + }); + it('should return the count', function() { + assert.equal(this.res.body.count, 1); + }); + }, + ); }); describe('/store/:id/widgets/count - filtered (no matches)', function() { beforeEach(function() { - this.url = '/api/stores/' + this.store.id + '/widgets/count?where[name]=bar'; - }); - lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets/count?where[name]=bar', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - }); - it('should return the count', function() { - assert.equal(this.res.body.count, 0); - }); + this.url = + '/api/stores/' + this.store.id + '/widgets/count?where[name]=bar'; }); + lt.describe.whenCalledRemotely( + 'GET', + '/api/stores/:id/widgets/count?where[name]=bar', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + }); + it('should return the count', function() { + assert.equal(this.res.body.count, 0); + }); + }, + ); }); describe('/widgets/:id/store', function() { beforeEach(function(done) { const self = this; - this.store.widgets.create({ - name: this.widgetName, - }, function(err, widget) { - self.widget = widget; - self.url = '/api/widgets/' + self.widget.id + '/store'; + this.store.widgets.create( + { + name: this.widgetName, + }, + function(err, widget) { + self.widget = widget; + self.url = '/api/widgets/' + self.widget.id + '/store'; - done(); - }); + done(); + }, + ); }); lt.describe.whenCalledRemotely('GET', '/api/widgets/:id/store', function() { it('should succeed with statusCode 200', function() { @@ -350,53 +422,74 @@ describe('relations - integration', function() { function setup(connecting, cb) { const root = {}; - async.series([ - // Clean up models - function(done) { - app.models.physician.destroyAll(function(err) { - app.models.patient.destroyAll(function(err) { - app.models.appointment.destroyAll(function(err) { - done(); + async.series( + [ + // Clean up models + function(done) { + app.models.physician.destroyAll(function(err) { + app.models.patient.destroyAll(function(err) { + app.models.appointment.destroyAll(function(err) { + done(); + }); }); }); - }); + }, + + // Create a physician + function(done) { + app.models.physician.create( + { + name: 'ph1', + }, + function(err, physician) { + root.physician = physician; + + done(); + }, + ); + }, + + // Create a patient + connecting ? + function(done) { + root.physician.patients.create( + { + name: 'pa1', + }, + function(err, patient) { + root.patient = patient; + root.relUrl = + '/api/physicians/' + + root.physician.id + + '/patients/rel/' + + root.patient.id; + + done(); + }, + ); + } : + function(done) { + app.models.patient.create( + { + name: 'pa1', + }, + function(err, patient) { + root.patient = patient; + root.relUrl = + '/api/physicians/' + + root.physician.id + + '/patients/rel/' + + root.patient.id; + + done(); + }, + ); + }, + ], + function(err, done) { + cb(err, root); }, - - // Create a physician - function(done) { - app.models.physician.create({ - name: 'ph1', - }, function(err, physician) { - root.physician = physician; - - done(); - }); - }, - - // Create a patient - connecting ? function(done) { - root.physician.patients.create({ - name: 'pa1', - }, function(err, patient) { - root.patient = patient; - root.relUrl = '/api/physicians/' + root.physician.id + - '/patients/rel/' + root.patient.id; - - done(); - }); - } : function(done) { - app.models.patient.create({ - name: 'pa1', - }, function(err, patient) { - root.patient = patient; - root.relUrl = '/api/physicians/' + root.physician.id + - '/patients/rel/' + root.patient.id; - - done(); - }); - }], function(err, done) { - cb(err, root); - }); + ); } describe('PUT /physicians/:id/patients/rel/:fk', function() { @@ -411,33 +504,37 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('PUT', '/api/physicians/:id/patients/rel/:fk', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - assert.equal(this.res.body.patientId, this.patient.id); - assert.equal(this.res.body.physicianId, this.physician.id); - }); - - it('should create a record in appointment', function(done) { - const self = this; - app.models.appointment.find(function(err, apps) { - assert.equal(apps.length, 1); - assert.equal(apps[0].patientId, self.patient.id); - - done(); + lt.describe.whenCalledRemotely( + 'PUT', + '/api/physicians/:id/patients/rel/:fk', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.patientId, this.patient.id); + assert.equal(this.res.body.physicianId, this.physician.id); }); - }); - it('should connect physician to patient', function(done) { - const self = this; - self.physician.patients(function(err, patients) { - assert.equal(patients.length, 1); - assert.equal(patients[0].id, self.patient.id); + it('should create a record in appointment', function(done) { + const self = this; + app.models.appointment.find(function(err, apps) { + assert.equal(apps.length, 1); + assert.equal(apps[0].patientId, self.patient.id); - done(); + done(); + }); }); - }); - }); + + it('should connect physician to patient', function(done) { + const self = this; + self.physician.patients(function(err, patients) { + assert.equal(patients.length, 1); + assert.equal(patients[0].id, self.patient.id); + + done(); + }); + }); + }, + ); }); describe('PUT /physicians/:id/patients/rel/:fk with data', function() { @@ -455,36 +552,41 @@ describe('relations - integration', function() { const NOW = Date.now(); const data = {date: new Date(NOW)}; - lt.describe.whenCalledRemotely('PUT', '/api/physicians/:id/patients/rel/:fk', data, function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - assert.equal(this.res.body.patientId, this.patient.id); - assert.equal(this.res.body.physicianId, this.physician.id); - assert.equal(new Date(this.res.body.date).getTime(), NOW); - }); - - it('should create a record in appointment', function(done) { - const self = this; - app.models.appointment.find(function(err, apps) { - assert.equal(apps.length, 1); - assert.equal(apps[0].patientId, self.patient.id); - assert.equal(apps[0].physicianId, self.physician.id); - assert.equal(apps[0].date.getTime(), NOW); - - done(); + lt.describe.whenCalledRemotely( + 'PUT', + '/api/physicians/:id/patients/rel/:fk', + data, + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.patientId, this.patient.id); + assert.equal(this.res.body.physicianId, this.physician.id); + assert.equal(new Date(this.res.body.date).getTime(), NOW); }); - }); - it('should connect physician to patient', function(done) { - const self = this; - self.physician.patients(function(err, patients) { - assert.equal(patients.length, 1); - assert.equal(patients[0].id, self.patient.id); + it('should create a record in appointment', function(done) { + const self = this; + app.models.appointment.find(function(err, apps) { + assert.equal(apps.length, 1); + assert.equal(apps[0].patientId, self.patient.id); + assert.equal(apps[0].physicianId, self.physician.id); + assert.equal(apps[0].date.getTime(), NOW); - done(); + done(); + }); }); - }); - }); + + it('should connect physician to patient', function(done) { + const self = this; + self.physician.patients(function(err, patients) { + assert.equal(patients.length, 1); + assert.equal(patients[0].id, self.patient.id); + + done(); + }); + }); + }, + ); }); describe('HEAD /physicians/:id/patients/rel/:fk', function() { @@ -499,19 +601,23 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('HEAD', '/api/physicians/:id/patients/rel/:fk', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - }); - }); + 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) { const self = this; setup(true, function(err, root) { - self.url = '/api/physicians/' + root.physician.id + - '/patients/rel/' + '999'; + self.url = + '/api/physicians/' + root.physician.id + '/patients/rel/' + '999'; self.patient = root.patient; self.physician = root.physician; @@ -519,11 +625,15 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('HEAD', '/api/physicians/:id/patients/rel/:fk', function() { - it('should succeed with statusCode 404', function() { - assert.equal(this.res.statusCode, 404); - }); - }); + 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() { @@ -558,38 +668,45 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/rel/:fk', function() { - it('should succeed with statusCode 204', function() { - assert.equal(this.res.statusCode, 204); - }); - - it('should remove the record in appointment', function(done) { - const self = this; - app.models.appointment.find(function(err, apps) { - assert.equal(apps.length, 0); - - done(); + lt.describe.whenCalledRemotely( + 'DELETE', + '/api/physicians/:id/patients/rel/:fk', + function() { + it('should succeed with statusCode 204', function() { + assert.equal(this.res.statusCode, 204); }); - }); - it('should remove the connection between physician and patient', function(done) { - const self = this; - // Need to refresh the cache - self.physician.patients(true, function(err, patients) { - assert.equal(patients.length, 0); + it('should remove the record in appointment', function(done) { + const self = this; + app.models.appointment.find(function(err, apps) { + assert.equal(apps.length, 0); - done(); + done(); + }); }); - }); - }); + + it('should remove the connection between physician and patient', function(done) { + const self = this; + // Need to refresh the cache + self.physician.patients(true, function(err, patients) { + assert.equal(patients.length, 0); + + done(); + }); + }); + }, + ); }); describe('GET /physicians/:id/patients/:fk', function() { before(function(done) { const self = this; setup(true, function(err, root) { - self.url = '/api/physicians/' + root.physician.id + - '/patients/' + root.patient.id; + self.url = + '/api/physicians/' + + root.physician.id + + '/patients/' + + root.patient.id; self.patient = root.patient; self.physician = root.physician; @@ -597,20 +714,27 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('GET', '/api/physicians/:id/patients/:fk', function() { - it('should succeed with statusCode 200', function() { - assert.equal(this.res.statusCode, 200); - assert.equal(this.res.body.id, this.physician.id); - }); - }); + lt.describe.whenCalledRemotely( + 'GET', + '/api/physicians/:id/patients/:fk', + function() { + it('should succeed with statusCode 200', function() { + assert.equal(this.res.statusCode, 200); + assert.equal(this.res.body.id, this.physician.id); + }); + }, + ); }); describe('DELETE /physicians/:id/patients/:fk', function() { before(function(done) { const self = this; setup(true, function(err, root) { - self.url = '/api/physicians/' + root.physician.id + - '/patients/' + root.patient.id; + self.url = + '/api/physicians/' + + root.physician.id + + '/patients/' + + root.patient.id; self.patient = root.patient; self.physician = root.physician; @@ -618,39 +742,43 @@ describe('relations - integration', function() { }); }); - lt.describe.whenCalledRemotely('DELETE', '/api/physicians/:id/patients/:fk', function() { - it('should succeed with statusCode 204', function() { - assert.equal(this.res.statusCode, 204); - }); - - it('should remove the record in appointment', function(done) { - const self = this; - app.models.appointment.find(function(err, apps) { - assert.equal(apps.length, 0); - - done(); + lt.describe.whenCalledRemotely( + 'DELETE', + '/api/physicians/:id/patients/:fk', + function() { + it('should succeed with statusCode 204', function() { + assert.equal(this.res.statusCode, 204); }); - }); - it('should remove the connection between physician and patient', function(done) { - const self = this; - // Need to refresh the cache - self.physician.patients(true, function(err, patients) { - assert.equal(patients.length, 0); + it('should remove the record in appointment', function(done) { + const self = this; + app.models.appointment.find(function(err, apps) { + assert.equal(apps.length, 0); - done(); + done(); + }); }); - }); - it('should remove the record in patient', function(done) { - const self = this; - app.models.patient.find(function(err, patients) { - assert.equal(patients.length, 0); + it('should remove the connection between physician and patient', function(done) { + const self = this; + // Need to refresh the cache + self.physician.patients(true, function(err, patients) { + assert.equal(patients.length, 0); - done(); + done(); + }); }); - }); - }); + + it('should remove the record in patient', function(done) { + const self = this; + app.models.patient.find(function(err, patients) { + assert.equal(patients.length, 0); + + done(); + }); + }); + }, + ); }); }); @@ -659,14 +787,14 @@ describe('relations - integration', function() { // Disable "Warning: overriding remoting type product" this.app.remotes()._typeRegistry._options.warnWhenOverridingType = false; - const product = app.registry.createModel( - 'product', - {id: 'string', name: 'string'}, - ); - const category = app.registry.createModel( - 'category', - {id: 'string', name: 'string'}, - ); + const product = app.registry.createModel('product', { + id: 'string', + name: 'string', + }); + const category = app.registry.createModel('category', { + id: 'string', + name: 'string', + }); app.model(product, {dataSource: 'db'}); app.model(category, {dataSource: 'db'}); @@ -678,24 +806,29 @@ describe('relations - integration', function() { beforeEach(function createProductsInCategory(done) { const test = this; - this.category.products.create({ - name: 'a-product', - }, function(err, product) { - if (err) return done(err); + this.category.products.create( + { + name: 'a-product', + }, + function(err, product) { + if (err) return done(err); - test.product = product; + test.product = product; - done(); - }); + done(); + }, + ); }); beforeEach(function createAnotherCategoryAndProduct(done) { - app.models.category.create({name: 'another-category'}, - function(err, cat) { - if (err) return done(err); + app.models.category.create({name: 'another-category'}, function( + err, + cat, + ) { + if (err) return done(err); - cat.products.create({name: 'another-product'}, done); - }); + cat.products.create({name: 'another-product'}, done); + }); }); afterEach(function(done) { @@ -705,25 +838,27 @@ describe('relations - integration', function() { it.skip('allows to find related objects via where filter', function(done) { // TODO https://github.com/strongloop/loopback-datasource-juggler/issues/94 const expectedProduct = this.product; - this.get('/api/products?filter[where][categoryId]=' + this.category.id) - .expect(200, function(err, res) { - if (err) return done(err); + this.get( + '/api/products?filter[where][categoryId]=' + this.category.id, + ).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.eql([ - { - id: expectedProduct.id, - name: expectedProduct.name, - }, - ]); + expect(res.body).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name, + }, + ]); - done(); - }); + done(); + }); }); it('allows to find related object via URL scope', function(done) { const expectedProduct = this.product; - this.get('/api/categories/' + this.category.id + '/products') - .expect(200, function(err, res) { + this.get('/api/categories/' + this.category.id + '/products').expect( + 200, + function(err, res) { if (err) return done(err); expect(res.body).to.eql([ @@ -734,28 +869,30 @@ describe('relations - integration', function() { ]); done(); - }); + }, + ); }); it('includes requested related models in `find`', function(done) { const expectedProduct = this.product; - const url = '/api/categories/findOne?filter[where][id]=' + - this.category.id + '&filter[include]=products'; + const url = + '/api/categories/findOne?filter[where][id]=' + + this.category.id + + '&filter[include]=products'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.have.property('products'); - expect(res.body.products).to.eql([ - { - id: expectedProduct.id, - name: expectedProduct.name, - }, - ]); + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name, + }, + ]); - done(); - }); + done(); + }); }); it.skip('includes requested related models in `findById`', function(done) { @@ -764,20 +901,19 @@ describe('relations - integration', function() { // Note: the URL format is not final const url = '/api/categories/' + this.category.id + '?include=products'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.have.property('products'); - expect(res.body.products).to.eql([ - { - id: expectedProduct.id, - name: expectedProduct.name, - }, - ]); + expect(res.body).to.have.property('products'); + expect(res.body.products).to.eql([ + { + id: expectedProduct.id, + name: expectedProduct.name, + }, + ]); - done(); - }); + done(); + }); }); }); @@ -790,10 +926,7 @@ describe('relations - integration', function() { ); app.model(group, {dataSource: 'db'}); - const poster = app.registry.createModel( - 'poster', - {url: 'string'}, - ); + const poster = app.registry.createModel('poster', {url: 'string'}); app.model(poster, {dataSource: 'db'}); group.embedsOne(poster, {as: 'cover'}); @@ -801,14 +934,13 @@ describe('relations - integration', function() { before(function createImage(done) { const test = this; - app.models.group.create({name: 'Group 1'}, - function(err, group) { - if (err) return done(err); + app.models.group.create({name: 'Group 1'}, function(err, group) { + if (err) return done(err); - test.group = group; + test.group = group; - done(); - }); + done(); + }); }); after(function(done) { @@ -821,9 +953,7 @@ describe('relations - integration', function() { this.post(url) .send({url: 'http://image.url'}) .expect(200, function(err, res) { - expect(res.body).to.be.eql( - {url: 'http://image.url'}, - ); + expect(res.body).to.be.eql({url: 'http://image.url'}); done(); }); @@ -832,32 +962,26 @@ describe('relations - integration', function() { it('includes the embedded models', function(done) { const url = '/api/groups/' + this.group.id; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body.name).to.be.equal('Group 1'); - expect(res.body.poster).to.be.eql( - {url: 'http://image.url'}, - ); + expect(res.body.name).to.be.equal('Group 1'); + expect(res.body.poster).to.be.eql({url: 'http://image.url'}); - done(); - }); + done(); + }); }); it('returns the embedded model', function(done) { const url = '/api/groups/' + this.group.id + '/cover'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql( - {url: 'http://image.url'}, - ); + expect(res.body).to.be.eql({url: 'http://image.url'}); - done(); - }); + done(); + }); }); it('updates an embedded model', function(done) { @@ -875,16 +999,13 @@ describe('relations - integration', function() { it('returns the updated embedded model', function(done) { const url = '/api/groups/' + this.group.id + '/cover'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql( - {url: 'http://changed.url'}, - ); + expect(res.body).to.be.eql({url: 'http://changed.url'}); - done(); - }); + done(); + }); }); it('deletes an embedded model', function(done) { @@ -909,7 +1030,8 @@ describe('relations - integration', function() { const todoItem = app.registry.createModel( 'todoItem', - {content: 'string'}, {forceId: false}, + {content: 'string'}, + {forceId: false}, ); app.model(todoItem, {dataSource: 'db'}); @@ -918,15 +1040,14 @@ describe('relations - integration', function() { before(function createTodoList(done) { const test = this; - app.models.todoList.create({name: 'List A'}, - function(err, list) { - if (err) return done(err); + 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); - }); + test.todoList = list; + list.items.build({content: 'Todo 1'}); + list.items.build({content: 'Todo 2'}); + list.save(done); + }); }); after(function(done) { @@ -936,50 +1057,45 @@ describe('relations - integration', function() { it('includes the embedded models', function(done) { const url = '/api/todo-lists/' + this.todoList.id; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - 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}, - ]); + 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(); - }); + done(); + }); }); it('returns the embedded models', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {content: 'Todo 1', id: 1}, - {content: 'Todo 2', id: 2}, - ]); + expect(res.body).to.be.eql([ + {content: 'Todo 1', id: 1}, + {content: 'Todo 2', id: 2}, + ]); - done(); - }); + done(); + }); }); it('filters the embedded models', function(done) { let url = '/api/todo-lists/' + this.todoList.id + '/items'; url += '?filter[where][id]=2'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {content: 'Todo 2', id: 2}, - ]); + expect(res.body).to.be.eql([{content: 'Todo 2', id: 2}]); - done(); - }); + done(); + }); }); it('creates embedded models', function(done) { @@ -999,59 +1115,53 @@ describe('relations - integration', function() { it('includes the created embedded model', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {content: 'Todo 1', id: 1}, - {content: 'Todo 2', id: 2}, - {content: 'Todo 3', id: 3}, - ]); + expect(res.body).to.be.eql([ + {content: 'Todo 1', id: 1}, + {content: 'Todo 2', id: 2}, + {content: 'Todo 3', id: 3}, + ]); - done(); - }); + done(); + }); }); it('returns an embedded model by (internal) id', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items/3'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql( - {content: 'Todo 3', id: 3}, - ); + expect(res.body).to.be.eql({content: 'Todo 3', id: 3}); - done(); - }); + done(); + }); }); it('removes an embedded model', function(done) { const expectedProduct = this.product; const url = '/api/todo-lists/' + this.todoList.id + '/items/2'; - this.del(url) - .expect(200, function(err, res) { - done(); - }); + this.del(url).expect(200, function(err, res) { + done(); + }); }); it('returns the embedded models - verify', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {content: 'Todo 1', id: 1}, - {content: 'Todo 3', id: 3}, - ]); + expect(res.body).to.be.eql([ + {content: 'Todo 1', id: 1}, + {content: 'Todo 3', id: 3}, + ]); - done(); - }); + done(); + }); }); it('returns a 404 response when embedded model is not found', function(done) { @@ -1060,7 +1170,9 @@ describe('relations - integration', function() { if (err) return done(err); expect(res.body.error.status).to.be.equal(404); - expect(res.body.error.message).to.be.equal('Unknown "todoItem" id "2".'); + expect(res.body.error.message).to.be.equal( + 'Unknown "todoItem" id "2".', + ); expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); done(); @@ -1070,63 +1182,59 @@ describe('relations - integration', function() { it.skip('checks if an embedded model exists - ok', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items/3'; - this.head(url) - .expect(200, function(err, res) { - done(); - }); + this.head(url).expect(200, function(err, res) { + done(); + }); }); it.skip('checks if an embedded model exists - fail', function(done) { const url = '/api/todo-lists/' + this.todoList.id + '/items/2'; - this.head(url) - .expect(404, function(err, res) { - done(); - }); + this.head(url).expect(404, function(err, res) { + done(); + }); }); }); describe('referencesMany', function() { before(function defineProductAndCategoryModels() { - const recipe = app.registry.createModel( - 'recipe', - {name: 'string'}, - ); + const recipe = app.registry.createModel('recipe', {name: 'string'}); app.model(recipe, {dataSource: 'db'}); - const ingredient = app.registry.createModel( - 'ingredient', - {name: 'string'}, - ); + const ingredient = app.registry.createModel('ingredient', { + name: 'string', + }); app.model(ingredient, {dataSource: 'db'}); - const photo = app.registry.createModel( - 'photo', - {name: 'string'}, - ); + const photo = app.registry.createModel('photo', {name: 'string'}); app.model(photo, {dataSource: 'db'}); recipe.referencesMany(ingredient); // contrived example for test: - recipe.hasOne(photo, {as: 'picture', options: { - http: {path: 'image'}, - }}); + recipe.hasOne(photo, { + as: 'picture', + options: { + http: {path: 'image'}, + }, + }); }); before(function createRecipe(done) { const test = this; - app.models.recipe.create({name: 'Recipe'}, - function(err, recipe) { - if (err) return done(err); + app.models.recipe.create({name: 'Recipe'}, function(err, recipe) { + if (err) return done(err); - test.recipe = recipe; - recipe.ingredients.create({ - name: 'Chocolate'}, + 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) { @@ -1151,15 +1259,14 @@ describe('relations - integration', function() { const url = '/api/recipes/' + this.recipe.id; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body.ingredientIds).to.eql([test.ingredient1]); - expect(res.body).to.not.have.property('ingredients'); + expect(res.body.ingredientIds).to.eql([test.ingredient1]); + expect(res.body).to.not.have.property('ingredients'); - done(); - }); + done(); + }); }); it('creates referenced models', function(done) { @@ -1180,35 +1287,33 @@ describe('relations - integration', function() { const url = '/api/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - {name: 'Sugar', id: test.ingredient2}, - {name: 'Butter', id: test.ingredient3}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + {name: 'Sugar', id: test.ingredient2}, + {name: 'Butter', id: test.ingredient3}, + ]); - done(); - }); + done(); + }); }); it('returns the referenced models', function(done) { const url = '/api/recipes/' + this.recipe.id + '/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - {name: 'Butter', id: test.ingredient3}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + {name: 'Butter', id: test.ingredient3}, + ]); - done(); - }); + done(); + }); }); it('filters the referenced models', function(done) { @@ -1216,16 +1321,13 @@ describe('relations - integration', function() { url += '?filter[where][name]=Butter'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Butter', id: test.ingredient3}, - ]); + expect(res.body).to.be.eql([{name: 'Butter', id: test.ingredient3}]); - done(); - }); + done(); + }); }); it('includes the referenced models', function(done) { @@ -1233,20 +1335,20 @@ describe('relations - integration', function() { url += '&filter[include]=ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - 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}, - ]); + 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(); - }); + done(); + }); }); it('returns a referenced model by id', function(done) { @@ -1254,16 +1356,13 @@ describe('relations - integration', function() { url += this.ingredient3; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql( - {name: 'Butter', id: test.ingredient3}, - ); + expect(res.body).to.be.eql({name: 'Butter', id: test.ingredient3}); - done(); - }); + done(); + }); }); it('keeps an array of ids - verify', function(done) { @@ -1272,15 +1371,14 @@ describe('relations - integration', function() { const expected = [test.ingredient1, test.ingredient3]; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body.ingredientIds).to.eql(expected); - expect(res.body).to.not.have.property('ingredients'); + expect(res.body.ingredientIds).to.eql(expected); + expect(res.body).to.not.have.property('ingredients'); - done(); - }); + done(); + }); }); it('destroys a referenced model', function(done) { @@ -1288,43 +1386,40 @@ describe('relations - integration', function() { let url = '/api/recipes/' + this.recipe.id + '/ingredients/'; url += this.ingredient3; - this.del(url) - .expect(200, function(err, res) { - done(); - }); + this.del(url).expect(200, function(err, res) { + done(); + }); }); it('has destroyed a referenced model', function(done) { const url = '/api/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - {name: 'Sugar', id: test.ingredient2}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + {name: 'Sugar', id: test.ingredient2}, + ]); - done(); - }); + done(); + }); }); it('returns the referenced models without the deleted one', function(done) { const url = '/api/recipes/' + this.recipe.id + '/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + ]); - done(); - }); + done(); + }); }); it('creates/links a reference by id', function(done) { @@ -1332,31 +1427,27 @@ describe('relations - integration', function() { url += '/rel/' + this.ingredient2; const test = this; - this.put(url) - .expect(200, function(err, res) { - expect(res.body).to.be.eql( - {name: 'Sugar', id: test.ingredient2}, - ); + this.put(url).expect(200, function(err, res) { + expect(res.body).to.be.eql({name: 'Sugar', id: test.ingredient2}); - done(); - }); + done(); + }); }); it('returns the referenced models - verify', function(done) { const url = '/api/recipes/' + this.recipe.id + '/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - {name: 'Sugar', id: test.ingredient2}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + {name: 'Sugar', id: test.ingredient2}, + ]); - done(); - }); + done(); + }); }); it('removes/unlinks a reference by id', function(done) { @@ -1364,77 +1455,69 @@ describe('relations - integration', function() { url += '/rel/' + this.ingredient1; const test = this; - this.del(url) - .expect(200, function(err, res) { - done(); - }); + this.del(url).expect(200, function(err, res) { + done(); + }); }); it('returns the referenced models without the unlinked one', function(done) { const url = '/api/recipes/' + this.recipe.id + '/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Sugar', id: test.ingredient2}, - ]); + expect(res.body).to.be.eql([{name: 'Sugar', id: test.ingredient2}]); - done(); - }); + done(); + }); }); it('has not destroyed an unlinked model', function(done) { const url = '/api/ingredients'; const test = this; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.eql([ - {name: 'Chocolate', id: test.ingredient1}, - {name: 'Sugar', id: test.ingredient2}, - ]); + expect(res.body).to.be.eql([ + {name: 'Chocolate', id: test.ingredient1}, + {name: 'Sugar', id: test.ingredient2}, + ]); - done(); - }); + done(); + }); }); it('uses a custom relation path', function(done) { const url = '/api/recipes/' + this.recipe.id + '/image'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(err).to.not.exist(); - expect(res.body.name).to.equal('Photo 1'); + expect(err).to.not.exist(); + expect(res.body.name).to.equal('Photo 1'); - done(); - }); + done(); + }); }); it.skip('checks if a referenced model exists - ok', function(done) { let url = '/api/recipes/' + this.recipe.id + '/ingredients/'; url += this.ingredient1; - this.head(url) - .expect(200, function(err, res) { - done(); - }); + this.head(url).expect(200, function(err, res) { + done(); + }); }); it.skip('checks if an referenced model exists - fail', function(done) { let url = '/api/recipes/' + this.recipe.id + '/ingredients/'; url += this.ingredient3; - this.head(url) - .expect(404, function(err, res) { - done(); - }); + this.head(url).expect(404, function(err, res) { + done(); + }); }); }); @@ -1489,8 +1572,13 @@ describe('relations - integration', function() { throw new Error('This should not crash the app'); }; - Page.remoteMethod('__throw__errors', {isStatic: false, http: {path: '/throws', verb: 'get'}, - accepts: [{arg: 'options', type: 'object', http: 'optionsFromRequest'}]}); + Page.remoteMethod('__throw__errors', { + isStatic: false, + http: {path: '/throws', verb: 'get'}, + accepts: [ + {arg: 'options', type: 'object', http: 'optionsFromRequest'}, + ], + }); // Now `pages` has nestRemoting set to true and no need to call nestRemoting() // Book.nestRemoting('pages'); @@ -1500,13 +1588,21 @@ describe('relations - integration', function() { expect(Book.prototype['__findById__pages']).to.be.a('function'); expect(Image.prototype['__get__book']).to.be.a('function'); - Page.beforeRemote('prototype.__findById__notes', function(ctx, result, next) { + 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) { + Page.afterRemote('prototype.__findById__notes', function( + ctx, + result, + next, + ) { ctx.res.set('x-after', 'after'); next(); @@ -1524,71 +1620,72 @@ describe('relations - integration', function() { before(function createBook(done) { const test = this; - app.models.Book.create({name: 'Book 1'}, - function(err, book) { + 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.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 createChapters(done) { - const test = this; - test.book.chapters.create({name: 'Chapter 1'}, - function(err, chapter) { - if (err) return done(err); - - test.chapter = chapter; - chapter.notes.create({text: 'Chapter Note 1'}, function(err, note) { - test.cnote = note; + test.page = page; + page.notes.create({text: 'Page Note 1'}, function(err, note) { + test.note = note; done(); }); }); + }); + }); + + before(function createChapters(done) { + const test = this; + test.book.chapters.create({name: 'Chapter 1'}, function(err, chapter) { + if (err) return done(err); + + test.chapter = chapter; + chapter.notes.create({text: 'Chapter Note 1'}, function(err, note) { + test.cnote = note; + + done(); + }); + }); }); before(function createCover(done) { const test = this; - app.models.Image.create({name: 'Cover 1', book: test.book}, - function(err, image) { - if (err) return done(err); + app.models.Image.create({name: 'Cover 1', book: test.book}, function( + err, + image, + ) { + if (err) return done(err); - test.image = image; + test.image = image; - done(); - }); + done(); + }); }); it('has regular relationship routes - pages', function(done) { const test = this; - this.get('/api/books/' + test.book.id + '/pages') - .expect(200, function(err, res) { - if (err) return done(err); + this.get('/api/books/' + test.book.id + '/pages').expect(200, function( + err, + res, + ) { + if (err) return done(err); - expect(res.body).to.be.an('array'); - expect(res.body).to.have.length(1); - expect(res.body[0].name).to.equal('Page 1'); + expect(res.body).to.be.an('array'); + expect(res.body).to.have.length(1); + expect(res.body[0].name).to.equal('Page 1'); - done(); - }); + done(); + }); }); it('has regular relationship routes - notes', function(done) { const test = this; - this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id) - .expect(200, function(err, res) { + this.get('/api/pages/' + test.page.id + '/notes/' + test.note.id).expect( + 200, + function(err, res) { if (err) return done(err); expect(res.headers['x-before']).to.equal('before'); @@ -1597,13 +1694,15 @@ describe('relations - integration', function() { expect(res.body.text).to.equal('Page Note 1'); done(); - }); + }, + ); }); it('has a basic error handler', function(done) { const test = this; - this.get('/api/books/unknown/pages/' + test.page.id + '/notes') - .expect(404, function(err, res) { + this.get('/api/books/unknown/pages/' + test.page.id + '/notes').expect( + 404, + function(err, res) { if (err) return done(err); expect(res.body.error).to.be.an('object'); @@ -1612,21 +1711,24 @@ describe('relations - integration', function() { expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); done(); - }); + }, + ); }); it('enables nested relationship routes - belongsTo find', function(done) { const test = this; - this.get('/api/images/' + test.image.id + '/book/pages') - .end(function(err, res) { - if (err) return done(err); + this.get('/api/images/' + test.image.id + '/book/pages').end(function( + err, + res, + ) { + if (err) return done(err); - expect(res.body).to.be.an('array'); - expect(res.body).to.have.length(1); - expect(res.body[0].name).to.equal('Page 1'); + expect(res.body).to.be.an('array'); + expect(res.body).to.have.length(1); + expect(res.body[0].name).to.equal('Page 1'); - done(); - }); + done(); + }); }); it('enables nested relationship routes - belongsTo findById', function(done) { @@ -1645,35 +1747,44 @@ describe('relations - integration', function() { it('enables nested relationship routes - hasMany find', function(done) { const test = this; - this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes') - .expect(200, function(err, res) { - if (err) return done(err); + this.get( + '/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes', + ).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body).to.be.an('array'); - expect(res.body).to.have.length(1); - expect(res.body[0].text).to.equal('Page Note 1'); + 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(); - }); + done(); + }); }); it('enables nested relationship routes - hasMany findById', function(done) { const test = this; - this.get('/api/books/' + test.book.id + '/pages/' + test.page.id + '/notes/' + test.note.id) - .expect(200, function(err, res) { - if (err) return done(err); + this.get( + '/api/books/' + + test.book.id + + '/pages/' + + test.page.id + + '/notes/' + + test.note.id, + ).expect(200, function(err, res) { + if (err) return done(err); - 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'); + 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(); - }); + done(); + }); }); it('passes options to nested relationship routes', function() { - return this.get(`/api/books/${this.book.id}/pages/${this.page.id}/notes/${this.note.id}`) + return this.get( + `/api/books/${this.book.id}/pages/${this.page.id}/notes/${this.note.id}`, + ) .expect(200) .then(res => { expect(accessOptions).to.have.property('accessToken'); @@ -1682,15 +1793,21 @@ describe('relations - integration', function() { it('should nest remote hooks of ModelTo - hasMany findById', function(done) { const test = this; - this.get('/api/books/' + test.book.id + '/chapters/' + test.chapter.id + '/notes/' + test.cnote.id) - .expect(200, function(err, res) { - if (err) return done(err); + this.get( + '/api/books/' + + test.book.id + + '/chapters/' + + test.chapter.id + + '/notes/' + + test.cnote.id, + ).expect(200, function(err, res) { + if (err) return done(err); - expect(res.headers['x-before']).to.be.undefined(); - expect(res.headers['x-after']).to.be.undefined(); + expect(res.headers['x-before']).to.be.undefined(); + expect(res.headers['x-after']).to.be.undefined(); - done(); - }); + done(); + }); }); it('should have proper http.path for remoting', function() { @@ -1709,18 +1826,21 @@ describe('relations - integration', function() { it('should catch error if nested function throws', function(done) { const test = this; - this.get('/api/books/' + test.book.id + '/pages/' + this.page.id + '/throws') - .end(function(err, res) { - if (err) return done(err); + this.get( + '/api/books/' + test.book.id + '/pages/' + this.page.id + '/throws', + ).end(function(err, res) { + if (err) return done(err); - expect(res.body).to.be.an('object'); - expect(res.body.error).to.be.an('object'); - expect(res.body.error.name).to.equal('Error'); - expect(res.body.error.statusCode).to.equal(500); - expect(res.body.error.message).to.equal('This should not crash the app'); + expect(res.body).to.be.an('object'); + expect(res.body.error).to.be.an('object'); + expect(res.body.error.name).to.equal('Error'); + expect(res.body.error.statusCode).to.equal(500); + expect(res.body.error.message).to.equal( + 'This should not crash the app', + ); - done(); - }); + done(); + }); }); }); @@ -1764,15 +1884,14 @@ describe('relations - integration', function() { it('should find the referenced model', function(done) { const url = '/api/customers/' + cust.id + '/profile'; - this.get(url) - .expect(200, function(err, res) { - if (err) return done(err); + this.get(url).expect(200, function(err, res) { + if (err) return done(err); - expect(res.body.points).to.be.eql(10); - expect(res.body.customerId).to.be.eql(cust.id); + expect(res.body.points).to.be.eql(10); + expect(res.body.customerId).to.be.eql(cust.id); - done(); - }); + done(); + }); }); it('should not create the referenced model twice', function(done) { @@ -1800,18 +1919,20 @@ describe('relations - integration', function() { it('should delete the referenced model', function(done) { const url = '/api/customers/' + cust.id + '/profile'; - this.del(url) - .expect(204, function(err, res) { - done(err); - }); + this.del(url).expect(204, function(err, res) { + done(err); + }); }); it('should not find the referenced model', function(done) { const url = '/api/customers/' + cust.id + '/profile'; - this.get(url) - .expect(404, function(err, res) { - done(err); - }); + this.get(url).expect(404, function(err, res) { + expect(res.body.error.message).to.be.equal( + 'No "profile" instance(s) found', + ); + expect(res.body.error.code).to.be.equal('MODEL_NOT_FOUND'); + done(err); + }); }); }); });