The NodeJS ExpressJS framework is fantastic for creating performant web applications. This post details how to integrate the Sequelize NodeJS package with ExpressJS to achieve ORM capability.
Versions
At thhe time of this post, the following relevant versions of packages were used:
- co (4.6.0)
- express (4.13.4)
- pg (6.1.0)
- pg-hstore (2.3.2)
- sequelize (3.24.1)
- umzug (1.11.0)
Assumptions
This tutorial assumes that you already have an ExpressJS application created and you are working within the directory of the project. The tutorial also assumes you are using something similar to dotenv for tracking configuration parameters, or at least provide the parameters as environment variables when running the application.
Process
First, install the Sequelize package and required dependencies:
$ npm install --save sequelize co pg umzug
sequelize
is package which will provide the ORM capability.co
is used for serializing the database creation/migration prior to launch of the ExpressJS listener. Given JavaScript’s asynchronous nature and callbacks, it’s best to serialize this initial launch process.pg
is used for creating the database itself if it has not yet been created (sequelize
does not currently provide this capability).umzug
provides the ability to run migrations within the code itself (rather than by using the sequelize command-line binary). It natively creates a database table to keep track of which migrations have been run for Sequelize.
Next, update your bin/www
file to create two functions prior to binding the listener. Note that the co
package expects a Promise resolve() for each step in the serial sequence:
...
// create a user, database, and grant privileges to the user on the database
var pg = require('pg');
function createDatabase(resolve, reject) {
// set up connection string for creating the database, using any AWS RDS env vars first
var dbUserName = (process.env.RDS_USERNAME || process.env.DATABASE_USERNAME);
var dbUserPass = (process.env.RDS_PASSWORD || process.env.DATABASE_PASSWORD);
var conStringPrefix = "pg://" +
dbUserName +
":" +
dbUserPass +
"@" +
(process.env.RDS_HOSTNAME || process.env.DATABASE_HOST) +
":" +
(process.env.RDS_PORT || process.env.DATABASE_PORT) +
"/";
var conStringInit = conStringPrefix + 'postgres';
var newDBName = (process.env.RDS_DB_NAME || process.env.DATABASE_NAME);
var conStringNewDB = conStringPrefix + newDBName;
// create the user, database, and grant privileges
// note that this is not very defensive, but serves its purpose for the tutorial
pg.connect(conStringInit, function(err, client, done) {
client.query('CREATE USER ' + dbUserName + ' WITH LOGIN PASSWORD \'' + dbUserPass + '\'', function(err) {
client.query('CREATE DATABASE ' + newDBName, function(err) {
client.query('GRANT ALL PRIVILEGES ON DATABASE ' + newDBName + ' TO ' + dbUserName, function(err) {
client.end();
resolve();
});
});
});
});
};
// run any database migrations that have not yet been run using Umzug linked to Sequelize
// note - require the sequelize initialized via the models/index.js file via the tutorial
var models = require('../models');
var Umzug = require('umzug');
function migrateDatabase(resolve, reject) {
var umzug = new Umzug({
storage: 'sequelize',
storageOptions: {
sequelize: models.sequelize
},
migrations: {
params: [models.sequelize.getQueryInterface(), models.sequelize.constructor]
}
});
// determine any pending migrations, and feed those into the executor
// again, more defensive programming required, but good enough for tutorial
umzug.pending().then(function(migrations) {
umzug.execute({
migrations: migrations.map(function(migration) { return migration['file']; }),
method: 'up'
}).then(function(migrations) {
resolve();
});
});
};
...
Once the above functions have been created in the file, wrap the listen invocation in the same
bin/www
file to ensure that the database is created and migrations are run prior to the application
launching and binding to the interface:
...
// set up database, then launch listener
// more defensive coding required - again, fine for tutorial
var co = require('co');
co(function*() {
yield new Promise(createDatabase);
yield new Promise(migrateDatabase);
}).then(function() {
server.listen(port);
...
});
...
The application will then be available for use following the server.listen(port);
line of code.
Extra - User Tracking
Assuming that you already have a migration for adding user information which includes email address
and number of logins as logins
, the following code snippett is a nice way to create a user upon
first login, and increment the total number of logins each time they log in (if they already exist):
// create the user if not exist with default values, otherwise, if already exists...
// increment counter for total number of logins for user
models.User.findOrCreate({
where: { email: req.user.email },
defaults: {
name: req.user.name,
logins: 1
}
})
.spread(function(user, created) {
if (typeof(created) !== 'undefined' && created == false) {
user.increment('logins', {by: 1});
}
});
Summary
With the above setup, each time the application launches, it will run through the following process (in sequence) prior to the application being available. Although the startup time may be a bit longer, it is actually a quite nice automated way to ensure that your application deployments always include the latest up to date migrations:
- Create the database user (ignore error if exists)
- Create the database (ignore error if exists)
- Grant the database user privileges to the database (ignore error if exists)
- Run any migration in the
migrations/
folder that have no yet been run - Launch the listener for the application, making it available
Credit
Contributions to some of the above were gleaned from: