Last active
March 29, 2020 00:57
-
-
Save austinmao/83524cecd87ea1685b32 to your computer and use it in GitHub Desktop.
Adding Mongoose to Sails in a manner that binds promisified Mongoose functions to `Model.mongoose` preserves blueprints (auto-generated action routes), and most importantly does not remove the Waterline ORM. Note: this is done with sails-babel (ES2015) hook.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Bootstrap | |
* (sails.config.bootstrap) | |
* | |
* An asynchronous bootstrap function that runs before your Sails app gets lifted. | |
* This gives you an opportunity to set up your data model, run jobs, or perform some special logic. | |
* | |
* For more information on bootstrapping your app, check out: | |
* http://sailsjs.org/#/documentation/reference/sails.config/sails.config.bootstrap.html | |
*/ | |
var Promise = require('bluebird') | |
var _ = require('lodash') | |
var glob = Promise.promisify(require('glob')) | |
var path = require('path') | |
var changeCase = require('change-case') | |
var mongoose = require('mongoose') | |
Promise.promisifyAll(mongoose.Model); | |
Promise.promisifyAll(mongoose.Model.prototype); | |
Promise.promisifyAll(mongoose.Query.prototype); | |
module.exports.bootstrap = function (cb) { | |
Promise = require('bluebird') | |
connectMongoose() | |
// create Model.mongoose promisifed functions | |
.then(bindMongooseToModels) | |
// ensure geoNear index for geojson locations | |
// remove this if you do not need 2dsphere indexes | |
// .then(ensureMongo2dSphereIndex) | |
// return callback to finish bootstrap | |
.then(function() { | |
return cb() | |
}) | |
function connectMongoose() { | |
// connect mongoose to mongodb | |
var config = sails.config.connections.mongo | |
var mongoUrl = config.url || 'mongodb://' + config.host + ':' + config.port + '/' + config.database | |
console.log('Connecting Mongoose to', mongoUrl) | |
mongoose.connect(mongoUrl); | |
// mongoose.createConnection(mongoUrl); | |
var db = mongoose.connection; | |
Promise.promisifyAll(db) | |
db.onAsync('error') | |
.then(function() { | |
console.error.bind(console, 'Mongoose connection error:') | |
throw new Error('Mongoose connection error') | |
}) | |
return db.onceAsync('open') | |
.then(function() { | |
console.log('Connected Mongoose to', mongoUrl) | |
}) | |
} // connectMongoose | |
/** | |
* bind promisified mongoose functions to Model.mongoose. doing this in | |
* bootstrap because waterline does something to functions in its build | |
* phase (probably promisifying them) | |
*/ | |
function bindMongooseToModels() { | |
return glob("api/models/*.js") | |
.then(function(files) { | |
var models = [] | |
_.each(files, function(file) { | |
var basename = path.basename(file, '.js'); | |
// console.log('basename:', basename) | |
models.push(basename) | |
}) | |
_.each(models, function(model) { | |
console.log('initing mongoose schema for model', model) | |
// define pascal and lowercase model names | |
var pascalCaseModelName = model | |
var lowerCaseModelName = changeCase.lowerCase(model) | |
// get waterline model object | |
var Model = sails.models[lowerCaseModelName] | |
// get mongoose schema | |
var schema = Model.schema | |
// if no schema, move to the next model | |
if (!schema) return | |
// set schema collection name | |
schema.set('collection', lowerCaseModelName) | |
// declare mongoose model | |
var mongooseModel = mongoose.model(pascalCaseModelName, schema) | |
// append promisifed mongoose model to waterline object | |
Model.mongoose = mongooseModel | |
}) // _.each | |
}) // .then | |
.catch(console.error) | |
} // bindMongooseToModels | |
/** | |
* Ensure we have 2dsphere index on Place so GeoSpatial queries can work! | |
* @return {promise} [nativeAsync promise fulfilling ensureIndexAsync] | |
*/ | |
function ensureMongo2dSphereIndex() { | |
Promise.promisifyAll(sails.models.place) | |
return sails.models.place.nativeAsync() | |
.then(Promise.promisifyAll) | |
.then(function(places) { | |
return places.createIndexAsync({ location: '2dsphere' }) | |
}) | |
} // ensureMongo2dSphereIndex | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* /api/blueprints/create.js | |
* | |
* @description :: Server-side logic for create blueprint (POST routes) | |
* @help :: See http://links.sailsjs.org/docs/blueprints | |
*/ | |
import actionUtil from 'sails/lib/hooks/blueprints/actionUtil' | |
import changeCase from 'change-case' | |
/** | |
* post /:modelIdentity | |
* | |
* An API call to find and return a single model instance from the data adapter | |
* using the specified criteria. If an id was specified, just the instance with | |
* that unique id will be returned. | |
* | |
* Optional: | |
* @param {String} callback - default jsonp callback param (i.e. the name of the js function returned) | |
* @param {*} * - other params will be used as `values` in the create | |
*/ | |
module.exports = function createRecord(req, res) { | |
// load model name from url | |
let modelName = req.options.model || req.options.controller; | |
// change 'modelName' string to 'ModelName' for models | |
modelName = changeCase.pascalCase(modelName) | |
// Create data object (monolithic combination of all parameters) | |
// Omit the blacklisted params (like JSONP callback param, etc.) | |
const data = actionUtil.parseValues(req); | |
// get mongoose model | |
let Model = actionUtil.parseModel(req) | |
// save data | |
Model.mongoose.createAsync(data) | |
// Send JSONP-friendly response if it's supported | |
.then(res.created) | |
// Differentiate between waterline-originated validation errors | |
// and serious underlying issues. Respond with badRequest if a | |
// validation error is encountered, w/ validation info. | |
.catch(res.negotiate) | |
}; | |
/***************************** | |
* TODO: add pubsub hook back * | |
***************************//* | |
// If we have the pubsub hook, use the model class's publish method | |
// to notify all subscribers about the created item | |
if (req._sails.hooks.pubsub) { | |
if (req.isSocket) { | |
Model.subscribe(req, newInstance); | |
Model.introduce(newInstance); | |
} | |
Model.publishCreate(newInstance.toJSON(), !req.options.mirror && req); | |
} | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* /api/models/Model.js | |
* | |
* @description :: mongoose model | |
* @docs :: http://sailsjs.org/#!documentation/models | |
*/ | |
import mongoose from 'mongoose' | |
let schema = new mongoose.Schema({ | |
/* mongoose schema goes here */ | |
}) // schema | |
exports.schema = schema |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* /api/blueprints/update.js | |
* | |
* @description :: Server-side logic for update blueprint (PUT routes) | |
* @help :: See http://links.sailsjs.org/docs/blueprints | |
*/ | |
import _ from 'lodash' | |
import actionUtil from 'sails/lib/hooks/blueprints/actionUtil' | |
import util from 'util' // TODO: check if this is necessary | |
/** | |
* Update One Record | |
* | |
* An API call to update a model instance with the specified `id`, | |
* treating the other unbound parameters as attributes. | |
* | |
* @param {Integer|String} id - the unique id of the particular record you'd like to update (Note: this param should be specified even if primary key is not `id`!!) | |
* @param * - values to set on the record | |
* | |
*/ | |
module.exports = function updateOneRecord(req, res) { | |
// Look up the model | |
let Model = actionUtil.parseModel(req); | |
// Locate and validate the required `id` parameter. | |
const pk = actionUtil.requirePk(req); | |
// Create `values` object (monolithic combination of all parameters) | |
// But omit the blacklisted params (like JSONP callback param, etc.) | |
const values = actionUtil.parseValues(req); | |
// Omit the path parameter `id` from values, unless it was explicitly defined | |
// elsewhere (body/query): | |
const idParamExplicitlyIncluded = ((req.body && req.body.id) || req.query.id); | |
if (!idParamExplicitlyIncluded) delete values.id; | |
// 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') { | |
sails.log.warn('Cannot change primary key via update blueprint; ignoring value sent for `' + Model.primaryKey + '`'); | |
} | |
delete values[Model.primaryKey]; | |
// find record by id | |
Model.mongoose.findByIdAsync(pk) | |
// add values to record then save | |
.then(record => { | |
if (!record) throw 'not found' // skip to catch if no record found | |
return _.extend(record, values, { updatedAt: Date.now() }) | |
}) | |
// save record | |
.then(record => record.saveAsync()) | |
// res json | |
.spread(json => res.json(json)) | |
// res not found or error | |
.catch(err => err === 'not found' ? res.notFound() : res.serverError(err)) | |
}; | |
/******************************* | |
* what used to be in update.js * | |
*****************************//* | |
// 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.) | |
Model.findOne(pk).populateAll().exec(function found(err, matchingRecord) { | |
if (err) return res.serverError(err); | |
if (!matchingRecord) return res.notFound(); | |
Model.update(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) | |
); | |
} | |
const 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.) | |
let Q = Model.findOne(updatedRecord[Model.primaryKey]); | |
Q = actionUtil.populateEach(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> | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment