sails
Version:
API-driven framework for building realtime apps, using MVC conventions (based on Express and Socket.io)
225 lines (177 loc) • 8.57 kB
JavaScript
/**
* Module dependencies
*/
var _ = require('@sailshq/lodash');
var async = require('async');
var formatUsageError = require('../formatUsageError');
/**
* Replace Records in Collection
*
* http://sailsjs.com/docs/reference/blueprint-api/replace
*
* Replace the associated records in the given collection with
* different records. For example, replace all of a user's pets.
*
*/
module.exports = function replaceCollection (req, res) {
var parseBlueprintOptions = req.options.parseBlueprintOptions || req._sails.config.blueprints.parseBlueprintOptions;
// Set the blueprint action for parseBlueprintOptions.
req.options.blueprintAction = 'replace';
var queryOptions = parseBlueprintOptions(req);
var Model = req._sails.models[queryOptions.using];
var relation = queryOptions.alias;
// The primary key of the parent record
var parentPk = queryOptions.targetRecordId;
var childPks = queryOptions.associatedIds;
var removedFromNotificationsToSend = [];
var existingChildPks = [];
// Get the relevant association attribute on the parent model.
var attr = Model.attributes[relation];
// Get the related ("child") model.
var relatedModel = req._sails.models[attr.model || attr.collection];
// Get the inverse attribute (if any) on the related model.
var inverseAttr = attr.via && relatedModel.attributes[attr.via];
async.auto({
// If this is a many-to-one relationship, get all of the existing child PKs so that we can
// inform them of their removal (if they're not in the new set), and get all of the parent PKs
notificationsForExistingParentsOfReplacementChildren: function(cb) {
// If there is no inverse attribute on the related model, then this is a via-less collection
// which uses an implicit join table, so there's no "stealing" of children.
if (!inverseAttr) { return cb(); }
// If the inverse relationship on the related model is a collection, then this is
// a many-to-many relationship, so again, no stolen children.
if (inverseAttr.collection) { return cb(); }
// Ok, this is a many-to-one relationship, so let's find all of the "replacement" children
// and add `removedFrom` notifications for each (if the current parent is different from the new parent).
var criteria = {};
criteria[relatedModel.primaryKey] = childPks;
criteria[attr.via] = {'!=': parentPk};
relatedModel.stream(criteria).select([attr.via]).eachRecord(function(childRecord, nextChild) {
if (childRecord[attr.via] !== null) {
removedFromNotificationsToSend.push({
id: childRecord[attr.via],
removedId: childRecord[relatedModel.primaryKey],
attribute: relation,
reverse: false
});
}
return nextChild();
}).exec(cb);
},
notificationsForExistingChildrenOfParent: function(cb) {
// If this is a many-to-many or a via-less relationship, then we can't query the related model
// to find the existing children of our parent. We'll have to just do a find + populate.
if (!inverseAttr || inverseAttr.collection) {
var parentCriteria = {};
parentCriteria[Model.primaryKey] = parentPk;
var populateCriteria = {
select: [relatedModel.primaryKey]
};
Model.findOne(parentCriteria).populate(relation, populateCriteria).exec(function(err, parentRecord) {
if (err) {return cb(err);}
_.each(parentRecord[relation], function(child) {
existingChildPks.push(child[relatedModel.primaryKey]);
if (!_.contains(childPks, child[relatedModel.primaryKey])) {
removedFromNotificationsToSend.push({
id: parentPk,
removedId: child[relatedModel.primaryKey],
attribute: relation,
reverse: true
});
}
});
return cb();
});//_∏_
return;
}//-•
// Otherwise, this is a many-to-one relationship, and we can query the related model.
var criteria = {
where: {},
select: [relatedModel.primaryKey]
};
criteria.where[attr.via] = parentPk;
relatedModel.stream(criteria).eachRecord(function(childRecord, nextChild) {
existingChildPks.push(childRecord[relatedModel.primaryKey]);
if (!_.contains(childPks, childRecord[relatedModel.primaryKey])) {
removedFromNotificationsToSend.push({
id: parentPk,
removedId: childRecord[relatedModel.primaryKey],
attribute: relation,
reverse: true
});
}
return nextChild();
}).exec(cb);
}
}, function(err) {
if (err) {
return res.serverError(err);
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: Use a database transaction here, if all of the involved models
// are using the same datastore, and if that datastore supports transactions.
// e.g.
// ```
// Model.getDatastore().transaction(function during(db, proceed){ ... })
// .exec(function afterwards(err, result){}));
// ```
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Model.replaceCollection(parentPk, relation, childPks).exec( function(err) {
if (err) {
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// FUTURE: When we support transactions in blueprints, if this request was
// able to use a transaction, handle E_UNIQUE by sending back a 409 ("Conflict")
// instead of a 5xx error-- since in that case, all of our changes from this
// blueprint action would have been rolled back along with the transaction
// upon failure.
//
// For example, we'd call `proceed(err)` like normal, then in the "afterwards"
// callback from `.transaction()`, we'd check for E_UNIQUE, and if we see it, call:
// ```
// return res.badRequest(err);
// ```
// - - - - - - - - - - - - - - - - - - - - - - - -
switch (err.name) {
// If this is a usage error coming back from Waterline,
// (e.g. a bad criteria), then respond w/ a 400 status code.
case 'UsageError': return res.badRequest(formatUsageError(err, req));
case 'AdapterError':
switch(err.code) {
// If there is a uniqueness error, then this collection have been simultaneously
// mucked around with by some other query that is editing records at the same time.
// (Because we know that uniqueness errors cannot happen because of duplicates
// in the array of child ids-- duplicate child ids are ignored.) So for now,
// we'll respond with a 500 error in this case, but see the note above about how
// this will be handled in the future.
case 'E_UNIQUE': return res.serverError(err);
// Any other kind of adapter error is unexpected, so use 500.
default: return res.serverError(err);
}//•
// Otherwise, it's some other unexpected error, so use 500.
default: return res.serverError(err);
}
}//-•
// Broadcast updates to subscribers of the child records.
if (req._sails.hooks.pubsub) {
// Subscribe to the model you're adding to, if this was a socket request
if (req.isSocket) { Model.subscribe(req, [parentPk]); }
// Publish to subscribed sockets
_.each(_.difference(childPks, existingChildPks), function(childPk) {
Model._publishAdd(parentPk, relation, childPk, !req.options.mirror && req);
});
if (removedFromNotificationsToSend.length) {
_.each(removedFromNotificationsToSend, function(notification) {
Model._publishRemove(notification.id, notification.attribute, notification.removedId, !req.options.mirror && req, {noReverse: !notification.reverse});
});
}
}
var query = Model.findOne(parentPk, queryOptions.populates).meta(queryOptions.meta);
query.exec(function(err, matchingRecord) {
if (err) { return res.serverError(err); }
if (!matchingRecord) { return res.serverError(); }
if (!matchingRecord[relation]) { return res.serverError(); }
return res.ok(matchingRecord);
});
}); // </ Model.replaceCollection(parentPk)>
});
};