I am trying to create a multi-tenant app (saas), where each client has its own database.
My situation is:
I created a middleware that would determine who the client is based on a subdomain, then retrieve the client's database connection info from a general database. I don't know how to establish a connection object for this client so as to be able to use in my controllers. And should I do this in the middleware or in a controller? And if it's in the model, how do i pass the connection string and parameters (I could use session, but I don't know how to access session from within model).
How do i do the following?
This is an example of my middleware, and i would like to create a mongoose connection which i would like to make dynamic (pass in client's connection info):
function clientlistener() { return function (req, res, next) { console.dir('look at my sub domain ' + req.subdomains[0]); // console.log(req.session.Client.name); if (req.session.Client && req.session.Client.name === req.subdomains[0]) { var options = session.Client.options; var url = session.Client.url var conn = mongoose.createConnection(url, options); next(); } } }
How do I access this connection object from inside the controller? Or from the model?
Thank you.
connect() method is the method of the MongoDB module of the Node. js which is used to connect the database with our Node. js Application. This is an asynchronous method of the MongoDB module.
Mongoose is not the only ODM library, there are hapijs/joi, MongoDB schemas, etc. And while Mongoose is good especially in areas of inferring data types, we should choose to use the MongoDB schema validation for schemas validation.
It's not mandatory to use Mongoose over the MongoDB Native API. However, there are some benefits to doing so.
This is to help others who may find themselves in similar situation as I did. I hope that it could be standardized. I dont think we should have to reinvent the wheel every time someone needs to make a multi-tenant application.
This example describes a multi-tenant structure with each client having its own database. Like i said there might be a better way of doing this, but because i didn't get help myself, this was my solution.
So here are the goals this solution targets:
in your app.js
file
app.use(clientListener()); // checks and identify valid clients app.use(setclientdb());// sets db for valid clients
I've created two middlewares :
clientListener
- to identify the client connecting,setclientdb
- gets client details from Master database, after client is identified, and then establishes connection to client database.I check who the client is by checking the subdomain from the request object. I do a bunch of checks to be sure the client is valid (I know the code is messy, and can be made cleaner). After ensuring the client is valid, I store the clients info in session. I also check that if the clients info is already stored in session, there is no need to query the database again. We just need to make sure that the request subdomain, matches that which is already stored in session.
var Clients = require('../models/clients'); var basedomain = dbConfig.baseDomain; var allowedSubs = {'admin':true, 'www':true }; allowedSubs[basedomain] = true; function clientlistener() { return function(req, res, next) { //console.dir('look at my sub domain ' + req.subdomains[0]); // console.log(req.session.Client.name); if( req.subdomains[0] in allowedSubs || typeof req.subdomains[0] === 'undefined' || req.session.Client && req.session.Client.name === req.subdomains[0] ){ //console.dir('look at the sub domain ' + req.subdomains[0]); //console.dir('testing Session ' + req.session.Client); console.log('did not search database for '+ req.subdomains[0]); //console.log(JSON.stringify(req.session.Client, null, 4)); next(); } else{ Clients.findOne({subdomain: req.subdomains[0]}, function (err, client) { if(!err){ if(!client){ //res.send(client); res.send(403, 'Sorry! you cant see that.'); } else{ console.log('searched database for '+ req.subdomains[0]); //console.log(JSON.stringify(client, null, 4)); //console.log(client); // req.session.tester = "moyo cow"; req.session.Client = client; return next(); } } else{ console.log(err); return next(err) } }); } } } module.exports = clientlistener;
I check everything again making sure that the client is valid. Then the connection to the client's database with the info retrieved from session is opened.
I also make sure to store all active connections into a global object, so as to prevent new connections to the database upon each request(we don't want to overload each clients mongodb server with connections).
var mongoose = require('mongoose'); //var dynamicConnection = require('../models/dynamicMongoose'); function setclientdb() { return function(req, res, next){ //check if client has an existing db connection /*** Check if client db is connected and pooled *****/ if(/*typeof global.App.clientdbconn === 'undefined' && */ typeof(req.session.Client) !== 'undefined' && global.App.clients[req.session.Client.name] !== req.subdomains[0]) { //check if client session, matches current client if it matches, establish new connection for client if(req.session.Client && req.session.Client.name === req.subdomains[0] ) { console.log('setting db for client ' + req.subdomains[0]+ ' and '+ req.session.Client.dbUrl); client = mongoose.createConnection(req.session.Client.dbUrl /*, dbconfigoptions*/); client.on('connected', function () { console.log('Mongoose default connection open to ' + req.session.Client.name); }); // When the connection is disconnected client.on('disconnected', function () { console.log('Mongoose '+ req.session.Client.name +' connection disconnected'); }); // If the Node process ends, close the Mongoose connection process.on('SIGINT', function() { client.close(function () { console.log(req.session.Client.name +' connection disconnected through app termination'); process.exit(0); }); }); //If pool has not been created, create it and Add new connection to the pool and set it as active connection if(typeof(global.App.clients) === 'undefined' || typeof(global.App.clients[req.session.Client.name]) === 'undefined' && typeof(global.App.clientdbconn[req.session.Client.name]) === 'undefined') { clientname = req.session.Client.name; global.App.clients[clientname] = req.session.Client.name;// Store name of client in the global clients array activedb = global.App.clientdbconn[clientname] = client; //Store connection in the global connection array console.log('I am now in the list of active clients ' + global.App.clients[clientname]); } global.App.activdb = activedb; console.log('client connection established, and saved ' + req.session.Client.name); next(); } //if current client, does not match session client, then do not establish connection else { delete req.session.Client; client = false; next(); } } else { if(typeof(req.session.Client) === 'undefined') { next(); } //if client already has a connection make it active else{ global.App.activdb = global.App.clientdbconn[req.session.Client.name]; console.log('did not make new connection for ' + req.session.Client.name); return next(); } } } } module.exports = setclientdb;
Since I am using a combination of mongoose and native mongo, We have to compile our models at run time. Please see below
Add this to your app.js
// require your models directory var models = require('./models'); // Create models using mongoose connection for use in controllers app.use(function db(req, res, next) { req.db = { User: global.App.activdb.model('User', models.agency_user, 'users') //Post: global.App.activdb.model('Post', models.Post, 'posts') }; return next(); });
Explanation:
Like I said earlier on I created a global object to store the active database connection object: global.App.activdb
Then I use this connection object to create (compile) mongoose model, after i store it in the db property of the req object: req.db
. I do this so that i can access my models in my controller like this for example.
Example of my Users controller:
exports.list = function (req, res) { req.db.User.find(function (err, users) { res.send("respond with a resource" + users + 'and connections ' + JSON.stringify(global.App.clients, null, 4)); console.log('Worker ' + cluster.worker.id + ' running!'); }); };
I will come back and clean this up eventually. If anyone wants to help me, that be nice.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With