diff --git a/lib/relationships.js b/lib/relationships.js index 2c55c07..d63756b 100644 --- a/lib/relationships.js +++ b/lib/relationships.js @@ -1,81 +1,84 @@ -'use strict' +"use strict"; -var _ = require('lodash') -var utils = require('./utils') -const linkRelatedModels = require( - './utilities/relationship-utils' -).linkRelatedModels +var _ = require("lodash"); +var utils = require("./utils"); +const linkRelatedModels = + require("./utilities/relationship-utils").linkRelatedModels; module.exports = function (app, options) { // get remote methods. // set strong-remoting for more information // https://github.com/strongloop/strong-remoting - var remotes = app.remotes() - var id, data, model + var remotes = app.remotes(); + var id, data, model; - remotes.before('**', function (ctx, next) { + remotes.before("**", function (ctx, next) { if (utils.shouldNotApplyJsonApi(ctx, options)) { - return next() + return next(); } - var allowedMethodNames = ['updateAttributes', 'patchAttributes'] - var methodName = ctx.method.name - if (allowedMethodNames.indexOf(methodName) === -1) return next() + var allowedMethodNames = ["updateAttributes", "patchAttributes"]; + var methodName = ctx.method.name; + if (allowedMethodNames.indexOf(methodName) === -1) return next(); - id = ctx.req.params.id - data = options.data - model = utils.getModelFromContext(ctx, app) + id = ctx.req.params.id; + data = options.data; + model = utils.getModelFromContext(ctx, app); - relationships(model, id, data).then(() => next()).catch(err => next(err)) - }) + relationships(model, id, data, ctx.args.options) + .then(() => next()) + .catch((err) => next(err)); + }); // for create - remotes.after('**', function (ctx, next) { + remotes.after("**", function (ctx, next) { if (utils.shouldNotApplyJsonApi(ctx, options)) { - return next() + return next(); } - if (ctx.method.name !== 'create') return next() + if (ctx.method.name !== "create") return next(); if (ctx.result && ctx.result.data) { - id = ctx.result.data.id - data = options.data - model = utils.getModelFromContext(ctx, app) - relationships(model, id, data).then(() => next()).catch(err => next(err)) - return + id = ctx.result.data.id; + data = options.data; + model = utils.getModelFromContext(ctx, app); + relationships(model, id, data, ctx.args.options) + .then(() => next()) + .catch((err) => next(err)); + return; } - next() - }) -} + next(); + }); +}; -function extractIdsFromResource (resource) { +function extractIdsFromResource(resource) { if (_.isArray(resource)) { - return _.map(resource, 'id') + return _.map(resource, "id"); } - return _.get(resource, 'id', null) + return _.get(resource, "id", null); } -function relationships (model, id, payload) { - if (!id || !model) return - const relationships = _.get(payload, 'data.relationships', {}) +function relationships(model, id, payload, ctxOptions) { + if (!id || !model) return; + const relationships = _.get(payload, "data.relationships", {}); return Promise.all( - Object.keys(relationships).map(relationName => { - const relationship = relationships[relationName] - const relationDefn = model.relations[relationName] - if (!relationDefn) return + Object.keys(relationships).map((relationName) => { + const relationship = relationships[relationName]; + const relationDefn = model.relations[relationName]; + if (!relationDefn) return; - const type = relationDefn.type - const modelTo = relationDefn.modelTo + const type = relationDefn.type; + const modelTo = relationDefn.modelTo; // don't handle belongsTo in relationships function - if (!modelTo || type === 'belongsTo') return + if (!modelTo || type === "belongsTo") return; - const data = extractIdsFromResource(relationship.data) - const from = { model, id } - const to = { model: modelTo, data } - return linkRelatedModels(relationName, from, to) + const data = extractIdsFromResource(relationship.data); + const from = { model, id }; + const to = { model: modelTo, data }; + return linkRelatedModels(relationName, from, to, ctxOptions); }) - ) + ); } diff --git a/lib/serializer.js b/lib/serializer.js index 0d355c1..41a8012 100644 --- a/lib/serializer.js +++ b/lib/serializer.js @@ -257,8 +257,7 @@ function parseRelations (data, relations, options) { } var relationship = null - - if (!_.isUndefined(fk) && relation.modelTo !== relation.modelFrom) { + if (!_.isUndefined(fk) && !relation.multiple) { if (_.isArray(fk)) { relationship = makeRelations(toType, fk, options) } else { @@ -272,8 +271,12 @@ function parseRelations (data, relations, options) { } else if (relation.type === 'belongsTo') { relationships[name].data = null } - }) + + if (relation.modelTo === relation.modelFrom) { + delete relationships[name].data + } + }) return relationships } @@ -505,4 +508,4 @@ function createCompoundIncludes ( compoundInclude.attributes = relationship return compoundInclude -} +} \ No newline at end of file diff --git a/lib/utilities/relationship-utils.js b/lib/utilities/relationship-utils.js index b30a365..09bd4e6 100644 --- a/lib/utilities/relationship-utils.js +++ b/lib/utilities/relationship-utils.js @@ -1,15 +1,15 @@ -'use strict' +"use strict"; -var _ = require('lodash') -var statusCodes = require('http-status-codes') -const utils = require('../utils') +var _ = require("lodash"); +var statusCodes = require("http-status-codes"); +const utils = require("../utils"); const RELATION_TYPES = Object.freeze({ HAS_MANY_THROUGH: 0, HAS_MANY: 1, HAS_ONE: 2, - BELONGS_TO: 3 -}) + BELONGS_TO: 3, +}); /* global module */ module.exports = { @@ -23,8 +23,8 @@ module.exports = { updateBelongsTo: updateBelongsTo, updateHasManyThrough: updateHasManyThrough, detectUpdateStrategy: detectUpdateStrategy, - linkRelatedModels: linkRelatedModels -} + linkRelatedModels: linkRelatedModels, +}; /** * Get the invalid includes error. @@ -33,25 +33,27 @@ module.exports = { * @param {String} message * @return {Error} */ -function getInvalidIncludesError (message) { +function getInvalidIncludesError(message) { var error = new Error( - message || 'JSON API resource does not support `include`' - ) - error.statusCode = statusCodes.BAD_REQUEST - error.code = statusCodes.BAD_REQUEST - error.status = statusCodes.BAD_REQUEST + message || "JSON API resource does not support `include`" + ); + error.statusCode = statusCodes.BAD_REQUEST; + error.code = statusCodes.BAD_REQUEST; + error.status = statusCodes.BAD_REQUEST; - return error + return error; } -function isLoopbackInclude (ctx) { - return ctx.args && ctx.args.filter +function isLoopbackInclude(ctx) { + return ctx.args && ctx.args.filter; } -function isJSONAPIInclude (req) { - return _.isPlainObject(req.query) && - req.query.hasOwnProperty('include') && +function isJSONAPIInclude(req) { + return ( + _.isPlainObject(req.query) && + req.query.hasOwnProperty("include") && req.query.include.length > 0 + ); } /** @@ -60,8 +62,8 @@ function isJSONAPIInclude (req) { * @MemberOf {RelationshipUtils} * @return {Boolean} */ -function isRequestingIncludes (ctx) { - return isLoopbackInclude(ctx) || isJSONAPIInclude(ctx.req) +function isRequestingIncludes(ctx) { + return isLoopbackInclude(ctx) || isJSONAPIInclude(ctx.req); } /** @@ -72,12 +74,12 @@ function isRequestingIncludes (ctx) { * @param {Object} query * @return {Array} */ -function getIncludesArray (query) { - var relationships = query.include.split(',') +function getIncludesArray(query) { + var relationships = query.include.split(","); return relationships.map(function (val) { - return val.trim() - }) + return val.trim(); + }); } /** @@ -86,49 +88,60 @@ function getIncludesArray (query) { * @memberOf {RelationshipUtils} * @return {Boolean} */ -function shouldIncludeRelationships (method) { - return method.toLowerCase() === 'get' +function shouldIncludeRelationships(method) { + return method.toLowerCase() === "get"; } -function updateHasMany ( +function updateHasMany( leftPKName, leftPKValue, RightModel, rightFKName, rightFKValues ) { - return RightModel.updateAll({ [leftPKName]: { inq: rightFKValues } }, { - [rightFKName]: leftPKValue - }) + return RightModel.updateAll( + { [leftPKName]: { inq: rightFKValues } }, + { + [rightFKName]: leftPKValue, + } + ) .then(() => RightModel.find({ where: { [rightFKName]: leftPKValue } })) - .then(models => { - const idsToUnset = _.difference(_.map(models, 'id'), rightFKValues) - return RightModel.updateAll({ id: { inq: idsToUnset } }, { - [rightFKName]: null - }) - }) + .then((models) => { + const idsToUnset = _.difference(_.map(models, "id"), rightFKValues); + return RightModel.updateAll( + { id: { inq: idsToUnset } }, + { + [rightFKName]: null, + } + ); + }); } -function updateHasOne ( +function updateHasOne( rightPKName, leftPKValue, RightModel, rightFKName, rightPKId ) { - return RightModel.updateAll({ [rightFKName]: leftPKValue }, { - [rightFKName]: null - }) - .then(() => { - if (rightPKId) { - return RightModel.updateAll({ [rightPKName]: rightPKId }, { - [rightFKName]: leftPKValue - }) - } - }) + return RightModel.updateAll( + { [rightFKName]: leftPKValue }, + { + [rightFKName]: null, + } + ).then(() => { + if (rightPKId) { + return RightModel.updateAll( + { [rightPKName]: rightPKId }, + { + [rightFKName]: leftPKValue, + } + ); + } + }); } -function updateBelongsTo ( +function updateBelongsTo( LeftModel, leftPKName, leftPKValue, @@ -136,66 +149,77 @@ function updateBelongsTo ( rightPKId ) { if (rightPKId === null) { - return LeftModel.updateAll({ [leftPKName]: leftPKValue }, { - [leftFKName]: null - }) + return LeftModel.updateAll( + { [leftPKName]: leftPKValue }, + { + [leftFKName]: null, + } + ); } - return LeftModel.updateAll({ [leftPKName]: leftPKValue }, { - [leftFKName]: rightPKId - }) + return LeftModel.updateAll( + { [leftPKName]: leftPKValue }, + { + [leftFKName]: rightPKId, + } + ); } -function updateHasManyThrough ( +function updateHasManyThrough( leftPKName, leftPKValue, PivotModel, leftFKName, rightFKName, rightPKName, - rightFKValues + rightFKValues, + ctxOptions ) { - return PivotModel.find({ where: { [leftFKName]: leftPKValue } }) - .then(models => { - const existingIds = models.map(model => model[rightFKName]) - const idsToDelete = _.difference(existingIds, rightFKValues) - return PivotModel.destroyAll({ - [leftFKName]: leftPKValue, - [rightFKName]: { inq: idsToDelete } - }) - .then(() => { - const idsToAdd = _.difference(rightFKValues, existingIds) - return PivotModel.create( - idsToAdd.map(id => ({ - [leftFKName]: leftPKValue, - [rightFKName]: id - })) - ) - }) - }) + return PivotModel.find({ where: { [leftFKName]: leftPKValue } }).then( + (models) => { + const existingIds = models.map((model) => model[rightFKName]); + const idsToDelete = _.difference(existingIds, rightFKValues); + return PivotModel.destroyAll( + { + [leftFKName]: leftPKValue, + [rightFKName]: { inq: idsToDelete }, + }, + ctxOptions + ).then(() => { + const idsToAdd = _.difference(rightFKValues, existingIds); + return PivotModel.create( + idsToAdd.map((id) => ({ + [leftFKName]: leftPKValue, + [rightFKName]: id, + })), + ctxOptions + ); + }); + } + ); } -function detectUpdateStrategy (Model, relationName) { - const relationDefn = Model.relations[relationName] - if (relationDefn.modelThrough) return RELATION_TYPES.HAS_MANY_THROUGH - if (relationDefn.type === 'hasMany') return RELATION_TYPES.HAS_MANY - if (relationDefn.type === 'hasOne') return RELATION_TYPES.HAS_ONE - if (relationDefn.type === 'belongsTo') return RELATION_TYPES.BELONGS_TO +function detectUpdateStrategy(Model, relationName) { + const relationDefn = Model.relations[relationName]; + if (relationDefn.modelThrough) return RELATION_TYPES.HAS_MANY_THROUGH; + if (relationDefn.type === "hasMany") return RELATION_TYPES.HAS_MANY; + if (relationDefn.type === "hasOne") return RELATION_TYPES.HAS_ONE; + if (relationDefn.type === "belongsTo") return RELATION_TYPES.BELONGS_TO; } -function linkRelatedModels (relationName, from, to) { - const LeftModel = from.model - const id = from.id - const RightModel = to.model - const data = to.data - const relationDefn = LeftModel.relations[relationName] - const strategy = detectUpdateStrategy(LeftModel, relationName) +function linkRelatedModels(relationName, from, to, ctxOptions) { + const LeftModel = from.model; + const id = from.id; + const RightModel = to.model; + const data = to.data; + const relationDefn = LeftModel.relations[relationName]; + const strategy = detectUpdateStrategy(LeftModel, relationName); if (strategy === RELATION_TYPES.HAS_MANY_THROUGH) { - const leftPKName = utils.primaryKeyForModel(LeftModel) - const rightPKName = utils.primaryKeyForModel(RightModel) - const PivotModel = relationDefn.modelThrough - const leftFKName = relationDefn.keyTo - const rightFKName = relationDefn.keyThrough + const leftPKName = utils.primaryKeyForModel(LeftModel); + const rightPKName = utils.primaryKeyForModel(RightModel); + const PivotModel = relationDefn.modelThrough; + const leftFKName = relationDefn.keyTo; + const rightFKName = relationDefn.keyThrough; return updateHasManyThrough( leftPKName, id, @@ -203,25 +227,29 @@ function linkRelatedModels (relationName, from, to) { leftFKName, rightFKName, rightPKName, - data - ) + data, + ctxOptions + ); } if (strategy === RELATION_TYPES.HAS_MANY) { - const leftPKName = utils.primaryKeyForModel(LeftModel) - const rightFKName = relationDefn.keyTo - return updateHasMany(leftPKName, id, RightModel, rightFKName, data) + if (relationDefn.polymorphic) { + return; + } + const leftPKName = utils.primaryKeyForModel(LeftModel); + const rightFKName = relationDefn.keyTo; + return updateHasMany(leftPKName, id, RightModel, rightFKName, data); } if (strategy === RELATION_TYPES.HAS_ONE) { - const rightPKName = utils.primaryKeyForModel(RightModel) - const rightFKName = relationDefn.keyTo - return updateHasOne(rightPKName, id, RightModel, rightFKName, data) + const rightPKName = utils.primaryKeyForModel(RightModel); + const rightFKName = relationDefn.keyTo; + return updateHasOne(rightPKName, id, RightModel, rightFKName, data); } if (strategy === RELATION_TYPES.BELONGS_TO) { - const leftPKName = utils.primaryKeyForModel(LeftModel) - const leftFKName = relationDefn.keyFrom - return updateBelongsTo(LeftModel, leftPKName, id, leftFKName, data) + const leftPKName = utils.primaryKeyForModel(LeftModel); + const leftFKName = relationDefn.keyFrom; + return updateBelongsTo(LeftModel, leftPKName, id, leftFKName, data); } }