示例#1
0
  profile: function(req, res) {
    if (!req.user) return res.forbidden();

    // Lookup for records that match the specified criteria
    var Model = actionUtil.parseModel(req),
        where = _.omit(actionUtil.parseCriteria(req), 'access_token'),
        query = Model.find()
          .where(where)
          .limit(actionUtil.parseLimit(req))
          .skip(actionUtil.parseSkip(req))
          .sort(actionUtil.parseSort(req));

    query.exec(function found(err, matchingRecords) {
      if (err) return res.serverError(err);

      matchingRecords = _.reject(matchingRecords, 'disabled');
      var ids  = matchingRecords.map(function(m) { return m.id }),
          reqId = req.user[0].id;

      async.map(ids, function(userId, cb) {
        sails.services.utils.user['getUser'](userId, reqId, req.user, function (err, user) {
          if (err) return cb(err);
          if (userId !== reqId && !_.contains(where.username, user.username)) {
            delete user.username;
          }
          delete user.emails;
          delete user.auths;
          cb(null, user);
        });
      }, function(err, results) {
        if (err) { return res.send(400, err); }
        res.ok(results);
      });
    });
  },
示例#2
0
module.exports = (req, res) => {
  _.set(req.options, 'criteria.blacklist', ['fields', 'populate', 'limit', 'skip', 'page', 'sort']);

  const populate = req.param('populate') ? req.param('populate').replace(/ /g, '').split(',') : [];
  const Model = actionUtil.parseModel(req);
  if(req.param('fields')){
    var fields = req.param('fields') != 'all' ? req.param('fields').replace(/ /g, '').split(',') : [];
  } else {
    var fields = Model.default_return || [];
  }
  const where = actionUtil.parseCriteria(req);
  const limit = actionUtil.parseLimit(req);
  const skip = (req.param('page')-1) * limit || actionUtil.parseSkip(req);
  const sort = actionUtil.parseSort(req);

  const query = Model.find(null, fields.length > 0 ? {select: fields} : null).where(where).limit(limit).skip(skip).sort(sort);
  const findQuery = _.reduce(_.intersection(populate, takeAlias(Model.associations)), populateAlias, query); //???

  findQuery
    .then(records => [records, {
        root: {
          criteria: where,
          limit: limit,
          start: skip+1,
          end: skip + limit,
          page: Math.floor(skip / limit)+1
        }
    }])
    .spread(res.ok)
    .catch(res.negotiate);
};
示例#3
0
文件: create.js 项目: gcba-odin/odin
        .then(record => {
            var model = (actionUtil.parseModel(req)).adapter.identity;

            LogService.log(req, record.id);

            LogService.winstonLog('info', model + ' created', {
                ip: req.ip,
                resource: record.id
            });
            var associations = [];

            _.forEach(builder._model.definition, function(value, key) {
                if (value.foreignKey) {
                    associations.push(key);
                }
            });
            //populate the response

            builder._model.find(record.id).populate(associations).exec(function(err, record) {
                if (err) res.negotiate(err);
                res.created(record[0], {
                    meta: builder.meta(record[0]),
                    links: builder.links(record[0])
                });

            });

        })
示例#4
0
 constructor(req, res, many) {
     this.req = req;
     this.res = res;
     // Don't forget to set 'many' in blueprints/find.js (eg, new Response.ResponseGET(req, res, true);
     this._many = many;
     this._model = _actionUtil.parseModel(this.req);
     this.result = {};
 }
示例#5
0
module.exports = function ModelPolicy (req, res, next) {

  var modelCache = sails.config._modelCache;
  req.options.modelIdentity = actionUtil.parseModel(req).identity;

  if(!modelCache || modelCache.length === 0){
    sails.log.verbose('ModelPolicy: Model Cache is Empty');
  }

  if (_.isEmpty(req.options.modelIdentity)) {
    sails.log.verbose('ModelPolicy: Model Identity is Empty');
    return next();
  }

  req.options.modelDefinition = sails.models[req.options.modelIdentity];
  req.model = modelCache[req.options.modelIdentity];

  if (_.isObject(req.model) && !_.isNull(req.model.id)) {

    if(_.isObject(req.model.permissions) && _.isObject(req.model.permissions.public)){

      var method = PermissionService.getMethod(req.method);

      //console.log(req.model.permissions.public, method);
      if(_.isObject(req.model.permissions.public[method]) && req.model.permissions.public[method].action){
        sails.log.verbose("Model modelUnlocked");
        req.options.modelUnlocked = true;
      }
    }

    return next();
  }
  else{

  }

  sails.log.warn('Model [', req.options.modelIdentity, '] not found in model cache');

  // if the model is not found in the cache for some reason, get it from the database
  sails.models[sails.config.permission.modelModelIdentity].findOne({ identity: req.options.modelIdentity })
    .then(function (model) {
      if (!_.isObject(model)) {
        req.options.unknownModel = true;

        if (!sails.config.permissions.allowUnknownModelDefinition) {
          return next(new Error('Model definition not found: '+ req.options.modelIdentity));
        }
        else {
          model = sails.models[req.options.modelIdentity];
        }
      }

      req.model = model;
      next();
      return null;
    })
    .catch(next);
};
示例#6
0
module.exports = function (req, res) {
  actionUtil.parseModel(req)
    .destroy(actionUtil.requirePk(req))
    .then(function (records) {
      if (!records[0]) return res.notFound();

      return res.noContent();
    })
    .catch(res.serverError);
};
示例#7
0
文件: count.js 项目: adamayres/peeps
module.exports = function getCount (req, res) {
  var Model = actionUtil.parseModel(req);
  var criteria = actionUtil.parseCriteria(req);

  Model.count(criteria, function(error, response) {
    if (error) {
      return res.serverError('database_error', error);
    }

    res.ok({count: response});
  });
};
export function expand(req, res) {

  var Model = actionUtil.parseModel(req);
  var relation = req.options.alias;
  if (!relation || !Model) return res.serverError();

  // Allow customizable blacklist for params.
  req.options.criteria = req.options.criteria || {};
  req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'sort', 'id', 'parentid'];

  var parentPk = req.param('parentid');

  // Determine whether to populate using a criteria, or the
  // specified primary key of the child record, or with no
  // filter at all.
  var childPk = actionUtil.parsePk(req);

  // Coerce the child PK to an integer if necessary
  if (childPk) {
    if (Model.attributes[Model.primaryKey].type == 'integer') {
      childPk = +childPk || 0;
    }
  }

  var where = childPk ? {id: [childPk]} : actionUtil.parseCriteria(req);

  var populate = sails.util.objCompact({
    where: where,
    skip: actionUtil.parseSkip(req),
    limit: actionUtil.parseLimit(req),
    sort: actionUtil.parseSort(req)
  });

  Model
    .findOne(parentPk)
    .populate(relation, populate)
    .exec(function found(err, matchingRecord) {
      if (err) return res.serverError(err);
      if (!matchingRecord) return res.notFound('No record found with the specified id.');
      if (!matchingRecord[relation]) return res.notFound(util.format('Specified record (%s) is missing relation `%s`', parentPk, relation));

      // Subcribe to instance, if relevant
      // TODO: only subscribe to populated attribute- not the entire model
      if (sails.hooks.pubsub && req.isSocket) {
        Model.subscribe(req, matchingRecord);
        actionUtil.subscribeDeep(req, matchingRecord);
      }

      return res.ok(matchingRecord[relation]);
    });
};
示例#9
0
  count: function count(request, response) {
    var Model = actionUtil.parseModel(request);

    Model
      .count(actionUtil.parseCriteria(request))
      .exec(function found(error, count) {
        if (error) {
          response.negotiate(error);
        } else {
          response.ok({count: count});
        }
      })
    ;
  }
示例#10
0
module.exports = function ( req, res, next ) {
  var blueprint = req.options.action;
 
  if ( blueprint === 'create' ) {
    var Model = actionUtil.parseModel( req );
    if ( req.session.user.id ) {
      sails.log.debug( 'Policy beforeCreate: Injecting req.user.id into "' + Model.identity + '" parameters.' );
      req.body[ Model.identity ].owner = req.session.user.id;
    } else {
      // exception for creating new users, otherwise any creative act needs a logged in user
      if ( Model.identity !== 'user' ) return res.forbidden( "Create blueprint needs an authenticated user!" );
    }
  }
 
  next();
 
};
示例#11
0
文件: search.js 项目: gcba-odin/odin
module.exports = (req, res) => {

    let q = req.param('query');
    if (!q)
        return res.badRequest(null, {message: 'You should specify a "query" parameter!'});
    q = decodeURI(q)
    var model = actionUtil.parseModel(req);
    _.forEach(model.definition, function(val, key) {
        if (val.type === 'string' && model.searchables && model.searchables.indexOf(key) !== -1) {
            if (key === 'slug')
                req.params[key] = slug(q, {lower: true})
            else
                req.params[key] = q;
            }
        });

    return find(req, res);
};
示例#12
0
module.exports = (req, res) => {
  // check for authorization on this action
  if(! sails.config.authorization.authorize_controller(req.options.controller, 'update', req.user))
    return res.forbidden();
  // check for authorization on this resource
  if(!sails.config.authorization.authorize_resource(req.record, 'update', req.user))
    return res.forbidden();

  const Model = actionUtil.parseModel(req);
  const pk = actionUtil.requirePk(req);
  var values = actionUtil.parseValues(req);
  // get the permitted fields to parse in the payload
  var permitted = [];
  var fields = sails.config.fields_helper.fieldsInfo[Model.identity];
  for (var i = 0; i < fields.length; i++) {
    permitted.push(fields[i].name);
  }
  var notAllowed = _.without(values, permitted);
  var permitted = _.pull(values, notAllowed);
  //values = _.pick(values, permitted);

  Model
    .update(pk, permitted)
    .then(function(updated){
      _.assign(updated, {'model': Model.identity});
      // handle update of the images if the model has a file type field
      if(AssetsService.hasAsset(Model.identity) && (req.record[AssetsService.hasAsset(Model.identity)] != updated[0][AssetsService.hasAsset(Model.identity)])){
        console.log('dentro if');
        var infos = AssetsService.getAssetInfos(req.record[AssetsService.hasAsset(Model.identity)], '/');
        AssetsService.deleteAssets(infos.name);
        var cuts = sails.config.services.assets.cuts;
        for (var i = 0; i < cuts[Model.identity].length; i++) {
          AssetsService.createCuts(updated[0][AssetsService.hasAsset(Model.identity)], cuts[Model.identity][i].name ,cuts[Model.identity][i].width, cuts[Model.identity][i].height);
        }
      }
      return res.ok(updated);
    })
    .catch(function(err){
      return res.negotiate(err);
    });
};
示例#13
0
module.exports = function (req, res) {
    if (actionUtil.parsePk(req)) {
        return require('./findOne')(req, res);
    }
    
    var ThisModel = actionUtil.parseModel(req),
        where = actionUtil.parseCriteria(req),
        limit = actionUtil.parseLimit(req),
        skip = actionUtil.parseSkip(req),
        sort = actionUtil.parseSort(req),
        query = ThisModel.find().where(where).limit(limit).skip(skip).sort(sort);

    query = actionUtil.populateEach(query, req);
    query.exec(function (error, records) {
        if (error) {
            return res.serverError(error);
        }

        ThisModel
            .count(where)
            .exec(function (error, count) {
                if (error) {
                    return res.serverError(error);
                }

                var metaInfo = {
                    start: skip,
                    end: skip + limit,
                    limit: limit,
                    total: count,
                    criteria: where
                };

                res.set('Content-Range', metaInfo.start + '-' + metaInfo.end + '/' + metaInfo.total);
                res.set('Content-Count', metaInfo.total);
                return res.ok(records, null, null, metaInfo);
            });
    });
};
示例#14
0
module.exports = function findRecords (req, res) {

  // Look up the model
  var Model = actionUtil.parseModel(req);


  // If an `id` param was specified, use the findOne blueprint action
  // to grab the particular instance with its primary key === the value
  // of the `id` param.   (mainly here for compatibility for 0.9, where
  // there was no separate `findOne` action)
  if ( actionUtil.parsePk(req) ) {
    return require('./findOne')(req,res);
  }

  // Lookup for records that match the specified criteria
  var query = Model.find()
  .where( actionUtil.parseCriteria(req) )
  .where({ isDeleted : false })
  .sort( actionUtil.parseSort(req) );
  // TODO: .populateEach(req.options);
  query = actionUtil.populateEach(query, req);
  query.exec(function found(err, matchingRecords) {
    if (err) return res.serverError(err);

    // Only `.watch()` for new instances of the model if
    // `autoWatch` is enabled.
    if (req._sails.hooks.pubsub && req.isSocket) {
      Model.subscribe(req, matchingRecords);
      if (req.options.autoWatch) { Model.watch(req); }
      // Also subscribe to instances of all associated models
      _.each(matchingRecords, function (record) {
        actionUtil.subscribeDeep(req, record);
      });
    }

    res.ok(matchingRecords);
  });
};
示例#15
0
文件: destroy.js 项目: gcba-odin/odin
        .then(record => {
            var model = (actionUtil.parseModel(req)).adapter.identity;

            if (_.isUndefined(record[0])) {
                LogService.winstonLog('error', model + ' not found', {
                    ip: req.ip
                });
                return res.notFound(null, {
                    meta: builder.meta(undefined),
                    links: builder.links(undefined)
                });
            }
            LogService.log(req, record[0].id);

            LogService.winstonLog('info', model + ' deleted', {
                ip: req.ip,
                resource: record[0].id
            });

            res.deleted(record[0], {
                meta: builder.meta(),
                links: builder.links()
            });
        })
module.exports = function findOneRecord (req, res) {
  
  const fn = '[findOneRecord]'

  var Model = actionUtil.parseModel(req);
  var pk = actionUtil.requirePk(req);

  sails.log(fn, 'looking for pk:', pk)

  var query = Model.findOne(pk);
  query = actionUtil.populateEach(query, req);
  query.exec(function found(err, matchingRecord) {
    if (err) return res.serverError(err);
    if(!matchingRecord) return res.notFound('No record found with the specified `id`.');

    if (sails.hooks.pubsub && req.isSocket) {
      Model.subscribe(req, matchingRecord);
      actionUtil.subscribeDeep(req, matchingRecord);
    }

    res.ok(matchingRecord);
  });

};
module.exports = function ModelPolicy (req, res, next) {
  var modelCache = sails.hooks['sails-permissions']._modelCache;
  req.options.modelIdentity = actionUtil.parseModel(req).identity;

  if (_.isEmpty(req.options.modelIdentity)) {
    return next();
  }

  req.options.modelDefinition = sails.models[req.options.modelIdentity];
  req.model = modelCache[req.options.modelIdentity];

  if (_.isObject(req.model) && !_.isNull(req.model.id)) {
    return next();
  }

  sails.log.warn('Model [', req.options.modelIdentity, '] not found in model cache');

  // if the model is not found in the cache for some reason, get it from the database
  Model.findOne({ identity: req.options.modelIdentity })
    .then(function (model) {
      if (!_.isObject(model)) {
        req.options.unknownModel = true;

        if (!sails.config.permissions.allowUnknownModelDefinition) {
          return next(new Error('Model definition not found: '+ req.options.modelIdentity));
        }
        else {
          model = sails.models[req.options.modelIdentity];
        }
      }

      req.model = model;
      next();
    })
    .catch(next);
};
示例#18
0
module.exports = function updateOneRecord (req, res) {

    // Look up the model
    var Model = actionUtil.parseModel(req);

    // Locate and validate the required `id` parameter.
    var pk = actionUtil.requirePk(req);

    // Default the value blacklist to just "id", so that models that have an
    // "id" field that is _not_ the primary key don't have the id field
    // updated mistakenly.  See https://github.com/balderdashy/sails/issues/3625
    req.options.values = req.options.values || {};
    req.options.values.blacklist = req.options.values.blacklist || ['id'];

    // Create `values` object (monolithic combination of all parameters)
    // But omit the blacklisted params (like JSONP callback param, etc.)
    var values = actionUtil.parseValues(req);

    // No matter what, don't allow changing the PK via the update blueprint
    // (you should just drop and re-add the record if that's what you really want)
    if (typeof values[Model.primaryKey] !== 'undefined' && values[Model.primaryKey] != pk) {
        req._sails.log.warn('Cannot change primary key via update blueprint; ignoring value sent for `' + Model.primaryKey + '`');
    }
    // Make sure the primary key is unchanged
    values[Model.primaryKey] = pk;

    // Find and update the targeted record.
    //
    // (Note: this could be achieved in a single query, but a separate `findOne`
    //  is used first to provide a better experience for front-end developers
    //  integrating with the blueprint API.)
    var query = Model.findOne(pk);
    // Populate the record according to the current "populate" settings
    query = actionUtil.populateRequest(query, req);

    query.exec(function found(err, matchingRecord) {

        if (err) return res.serverError(err);
        if (!matchingRecord) return res.notFound();
        // delete values.documents;

        return Model.update({id:pk}, values).exec(function updated(err, records) {

            // Differentiate between waterline-originated validation errors
            // and serious underlying issues. Respond with badRequest if a
            // validation error is encountered, w/ validation info.
            if (err) return res.negotiate(err);


            // Because this should only update a single record and update
            // returns an array, just use the first item.  If more than one
            // record was returned, something is amiss.
            if (!records || !records.length || records.length > 1) {
                req._sails.log.warn(
                    util.format('Unexpected output from `%s.update`.', Model.globalId)
                );
            }

            var updatedRecord = records[0];

            // If we have the pubsub hook, use the Model's publish method
            // to notify all subscribers about the update.
            if (req._sails.hooks.pubsub) {
                if (req.isSocket) { Model.subscribe(req, records); }
                Model.publishUpdate(pk, _.cloneDeep(values), !req.options.mirror && req, {
                    previous: _.cloneDeep(matchingRecord.toJSON())
                });
            }

            // Do a final query to populate the associations of the record.
            //
            // (Note: again, this extra query could be eliminated, but it is
            //  included by default to provide a better interface for integrating
            //  front-end developers.)
            var Q = Model.findOne(updatedRecord[Model.primaryKey]);
            Q = actionUtil.populateRequest(Q, req);
            Q.exec(function foundAgain(err, populatedRecord) {
                if (err) return res.serverError(err);
                if (!populatedRecord) return res.serverError('Could not find record after updating!');
                res.ok(populatedRecord);
            }); // </foundAgain>
        });// </updated>
    }); // </found>
};
示例#19
0
module.exports = function expand(req, res) {

    function getRelationModel(relation) {
        const association = _.find(req.options.associations, {alias: relation});
        const collection = association.collection;
        const model = req._sails.models[collection];
        return model;
    }

    const Model = actionUtil.parseModel(req);
    const relation = req.options.alias;
    if (!relation || !Model)
        return res.serverError();

    const relationModel = getRelationModel(relation);

    // Allow customizable blacklist for params.
    req.options.criteria = req.options.criteria || {};
    req.options.criteria.blacklist = req.options.criteria.blacklist || ['limit', 'skip', 'sort', 'id', 'parentid'];

    const parentPk = req.param('parentid');

    // Determine whether to populate using a criteria, or the
    // specified primary key of the child record, or with no
    // filter at all.
    let childPk = actionUtil.parsePk(req);

    // Coerce the child PK to an integer if necessary
    if (childPk && Model.attributes[Model.primaryKey].type == 'integer')
        childPk = +childPk || 0;

    const where = childPk ? {id: [childPk]} : actionUtil.parseCriteria(req);

    const populate = sails.util.objCompact({where: where});

    delete populate.where.populate;

    let sort = actionUtil.parseSort(req);

    if (_.isEmpty(sort) && relationModel.DEFAULT_SORTING)
        sort = relationModel.DEFAULT_SORTING;
    const hardLimit = 1500;
    const skip = actionUtil.parseSkip(req);
    const limit = hardLimit + skip;
    populate.sort = sort;
    populate.limit = limit;
    populate.skip = skip;

    Model.findOne(parentPk)
        .populate(relation, populate)
        .then(matchingRecord => {
            if (!matchingRecord)
                return res.notFound('No record found with the specified id.');
            if (!matchingRecord[relation])
                return res.notFound(util.format('Specified record (%s) is missing relation `%s`', parentPk, relation));

            const count = matchingRecord[relation].length + skip;
            const relationsRecords = matchingRecord[relation];
            const limit = actionUtil.parseLimit(req);
            const recordsId = _.slice(_.map(relationsRecords, 'id'), 0, limit);

            let populateFields = req.param('populate');
            if (populateFields && !_.isArray(populateFields))
                populateFields = [populateFields];
            populateFields = _.filter(populateFields, f => _.some(relationModel.associations, {alias: f}));
            //sTODO add support for deep populate
            const where = {'id': recordsId};
            let query = relationModel.find({where, sort});
            _.forEach(populateFields, f => query = query.populate(f));

            return Promise.all([query, count])
                .spread((matchingRecords, count) => {
                    //if asking for a single related entity
                    if (childPk)
                        if (matchingRecords.length)
                            return res.ok(matchingRecords);
                        else
                            return res.notFound();
                    const postPopulate = _.get(Model, '_attributes.' + relation + '._postPopulate') || ((xs, id) => Promise.resolve(xs));

                    return postPopulate(matchingRecords, parentPk)
                        .then(newRecords => res.ok({
                                count: count,
                                items: newRecords
                            })
                        );
                })
                .catch(err => res.serverError(err));

        })
        .catch(err => res.serverError(err));
};
示例#20
0
"use strict";

/**
 * HeadController
 * @description :: Server-side logic for ...
 */

const actionUtil = require('sails/lib/hooks/blueprints/actionUtil');

module.exports = {
    head(req, res) {
        // const fields = req.param('fields') ? req.param('fields').replace(/ /g, '').split(',') : [];
        var model = actionUtil.parseModel(req);
        var id = req.param('id');
        res.set({
            'Authorization': 'JWT [token]',
            'Connection': 'keep-alive'
        });
        if (id) {
            model.findOne({
                id: id
            }).exec(function(err, record) {
                if (err) return res.negotiate(err);
                if (!record) return res.notFound;
                else return res.send(204);
            });
        }
        res.status = 204;
        return res.end();
    }
};
示例#21
0
module.exports = function findRecords(req, res) {

  // Look up the model
  var Model = actionUtil.parseModel(req);

  // Lookup for records that match the specified criteria
  var queryData = Model.find()
    .where( parseNgAdminFilter(actionUtil.parseCriteria(req)) )
    .limit( actionUtil.parseLimit(req) )
    .skip( actionUtil.parseSkip(req) );

  var sort = actionUtil.parseSort(req);
  if(
      sort != 'undefined undefined'
        &&
      sort != 'id DESC'
  ) {
    queryData.sort(actionUtil.parseSort(req));
  }

  queryData = actionUtil.populateEach(queryData, req);

  var queryCount = Model.count().where(actionUtil.parseCriteria(req));

  // Expose header to the client
  res.set('Access-Control-Expose-Headers', 'X-Total-Count');

  async.parallel(
    { data: getData, count: getTotalCount },
    function (err, results) {
      res.set('X-Total-Count', results.count);
      res.ok(results.data);
    }
  );

  function getTotalCount(cb) {
    queryCount.exec(function (err, count) {
      cb(null, count);
    });
  }

  function getData(cb) {
    queryData.exec(function found(err, matchingRecords) {
      if (err) return res.serverError(err);
      // Only `.watch()` for new instances of the model if
      // `autoWatch` is enabled.
      if (req._sails.hooks.pubsub && req.isSocket) {
        Model.subscribe(req, matchingRecords);
        if (req.options.autoWatch) { Model.watch(req); }
        // Also subscribe to instances of all associated models
        _.each(matchingRecords, function (record) {
          actionUtil.subscribeDeep(req, record);
        });
      }

      cb(null, matchingRecords);
    });
  }

  function parseNgAdminFilter(req){
    if(req._filters){
      if(JSON.parse(req._filters).category_id){
        return JSON.parse(req._filters);
      }else if(JSON.parse(req._filters).q){
        return {title: {like: '%'+JSON.parse(req._filters).q+'%'}};
      }
    }
    return req;
  }

};
export function addToCollection (req, res) {

  // Ensure a model and alias can be deduced from the request.
  var Model = actionUtil.parseModel(req);
  var relation = req.options.alias;
  if (!relation) {
    return res.serverError(new Error('Missing required route option, `req.options.alias`.'));
  }

  // The primary key of the parent record
  var parentPk = req.param('parentid');

  // Get the model class of the child in order to figure out the name of
  // the primary key attribute.
  var associationAttr = _.findWhere(Model.associations, { alias: relation });
  var ChildModel = sails.models[associationAttr.collection];
  var childPkAttr = ChildModel.primaryKey;


  // The child record to associate is defined by either...
  var child;

  // ...a primary key:
  var supposedChildPk = actionUtil.parsePk(req);
  if (supposedChildPk) {
    child = {};
    child[childPkAttr] = supposedChildPk;
  }
  // ...or an object of values:
  else {
    req.options.values = req.options.values || {};
    req.options.values.blacklist = req.options.values.blacklist || ['limit', 'skip', 'sort', 'id', 'parentid'];
    child = actionUtil.parseValues(req);
  }

  if (!child) {
    res.badRequest('You must specify the record to add (either the primary key of an existing record to link, or a new object without a primary key which will be used to create a record then link it.)');
  }


  var createdChild = false;

  async.auto({

    // Look up the parent record
    parent: function (cb) {
      Model.findOne(parentPk).exec(function foundParent(err, parentRecord) {
        if (err) return cb(err);
        if (!parentRecord) return cb({status: 404});
        if (!parentRecord[relation]) return cb({status: 404});
        cb(null, parentRecord);
      });
    },

    // If a primary key was specified in the `child` object we parsed
    // from the request, look it up to make sure it exists.  Send back its primary key value.
    // This is here because, although you can do this with `.save()`, you can't actually
    // get ahold of the created child record data, unless you create it first.
    actualChildPkValue: ['parent', function(cb) {

      // Below, we use the primary key attribute to pull out the primary key value
      // (which might not have existed until now, if the .add() resulted in a `create()`)

      // If the primary key was specified for the child record, we should try to find
      // it before we create it.
      if (child[childPkAttr]) {
        ChildModel.findOne(child[childPkAttr]).exec(function foundChild(err, childRecord) {
          if (err) return cb(err);
          // Didn't find it?  Then try creating it.
          if (!childRecord) {return createChild();}
          // Otherwise use the one we found.
          return cb(null, childRecord[childPkAttr]);
        });
      }
      // Otherwise, it must be referring to a new thing, so create it.
      else {
        return createChild();
      }

      // Create a new instance and send out any required pubsub messages.
      function createChild() {
        ChildModel.create(child).exec(function createdNewChild (err, newChildRecord){
          if (err) return cb(err);
          if (req._sails.hooks.pubsub) {
            if (req.isSocket) {
              ChildModel.subscribe(req, newChildRecord);
              ChildModel.introduce(newChildRecord);
            }
            ChildModel.publishCreate(newChildRecord, !req.options.mirror && req);
          }

          createdChild = true;
          return cb(null, newChildRecord[childPkAttr]);
        });
      }

    }],

    // Add the child record to the parent's collection
    add: ['parent', 'actualChildPkValue', function(cb, async_data) {
      try {
        // `collection` is the parent record's collection we
        // want to add the child to.
        var collection = async_data.parent[relation];
        collection.add(async_data.actualChildPkValue);
        return cb();
      }
      // Ignore `insert` errors
      catch (err) {
        // if (err && err.type !== 'insert') {
        if (err) {
          return cb(err);
        }
        // else if (err) {
        //   // if we made it here, then this child record is already
        //   // associated with the collection.  But we do nothing:
        //   // `add` is idempotent.
        // }

        return cb();
      }
    }]
  },

  // Save the parent record
  function readyToSave (err, async_data) {

    if (err) return res.negotiate(err);
    async_data.parent.save(function saved(err) {

      // Ignore `insert` errors for duplicate adds
      // (but keep in mind, we should not publishAdd if this is the case...)
      var isDuplicateInsertError = (err && typeof err === 'object' && err.length && err[0] && err[0].type === 'insert');
      if (err && !isDuplicateInsertError) return res.negotiate(err);

      // Only broadcast an update if this isn't a duplicate `add`
      // (otherwise connected clients will see duplicates)
      if (!isDuplicateInsertError && req._sails.hooks.pubsub) {

        // Subscribe to the model you're adding to, if this was a socket request
        if (req.isSocket) { Model.subscribe(req, async_data.parent); }
          // Publish to subscribed sockets
        Model.publishAdd(async_data.parent[Model.primaryKey], relation, async_data.actualChildPkValue, !req.options.mirror && req, {noReverse: createdChild});
      }

      // Finally, look up the parent record again and populate the relevant collection.
      // TODO: populateEach
      Model.findOne(parentPk).populate(relation).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);
      });
    });

  }); // </async.auto>
};