Skip to content

Instantly share code, notes, and snippets.

@austinmao
Last active March 29, 2020 00:57
Show Gist options
  • Save austinmao/83524cecd87ea1685b32 to your computer and use it in GitHub Desktop.
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.
/**
* 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
};
/**
* /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);
}
*/
/**
* /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
/**
* /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