Skip to content

Commit

Permalink
Implement API, tests, build infrastructure
Browse files Browse the repository at this point in the history
* Complete the implementation of the API to POST/GET messages.
* Add tests for /messages URLs.
* Add build infrastructure for linting and running tests.
* Add support for different database back-ends via Sequelize.
* Add migration for the messages table on the database.
  • Loading branch information
townxelliot committed Aug 5, 2016
1 parent c7e49f6 commit 4050789
Show file tree
Hide file tree
Showing 14 changed files with 616 additions and 34 deletions.
123 changes: 106 additions & 17 deletions README.md
Expand Up @@ -2,42 +2,131 @@

A simple web service to store and retrieve messages (strings of text).

(This is a place for me to keep my work while learning Express + friends.)
Example usage of the service (see the next section for details
of how to get it running):

Example usage of the service:

$ curl $domain/messages/ -d 'my test message to store'
$ curl http://localhost:3000/messages/ -d 'my test message to store'
{"id":12345}

$ curl $domain/messages/12345
$ curl http://localhost:3000/messages/12345
my test message to store

# Running the application

The application can be run from a git clone of the project. After cloning
it, install the node dependencies with:

npm install .

To start the application (including the HTTP server and the database
access layer) on Linux (or similar *nix system), do:

./bin/message-store

On Windows you can try:

node .\src\main.js

(I haven't tested this yet.)

By default, the application runs at http://localhost:3000.
(I plan to make this configurable later.)

# Development

???
You can lint the code (src/) with:

gulp lint

You can lint the tests (test/) with:

gulp lint-tests

# Running the test suite

???
To run the integration test suite, do:

gulp test-integration

This starts the whole application, running the HTTP server (on a random port
higher than 8000) and configuring the database.

Note that the test suite uses an in-memory SQLite database. If you modify the
tests to use a persistent database, you will need to modify the code to
ensure that the database is emptied before each test runs (see the beforeEach()
hook in the integration tests under test/integration/).

# Database configuration

The connection to the database is managed by
[Sequelize](http://docs.sequelizejs.com/en/latest/). See the
[Sequelize docs](http://docs.sequelizejs.com/en/latest/docs/getting-started/)
for further options not covered here.

By default, the application uses an in-memory SQLite database to store
posted messages. However, by creating a JSON configuration file for the
database connection, you can make the message storage persistent.

Database configuration is stored in a JSON file with this format:

{
"dialect": "sqlite|mysql|postgres|mssql",

# only for SQLite
"storage": "/path/to/sqlite/file",

# only for non-SQLite
"database": "databasename",
"username": "username",
"password": "password",
"host": "host",

# alternative to "host" if using a socket for MySQL
"socketPath": "/path/to/mysql/socket"
}

You can tell message-store where the database configuration is by setting
the `MESSAGE_STORE_DB_CONFIG` environment variable, e.g.

MESSAGE_STORE_DB_CONFIG=/home/me/my-config.json ./bin/message-store

Note that if you are using mysql, postgres or mssql as the dialect, you will
also need to install additional npm packages (message-store only installs the
SQLite package):

* Postgres: npm install pg pg-hstore
* MySQL: npm install mysql
* MSSQL: npm install tedious

## Examples

SQLite persisting to a file:

# Installation
{
"dialect": "sqlite",
"storage": "/home/me/message-storage.sqlite"
}

???
MySQL configuration (using LAMPP socket):

# Running the service
{
"dialect": "mysql",
"database": "message-store",
"username": "message-store",
"password": "password",
"socketPath": "/opt/lampp/var/mysql/mysql.sock"
}

???
NB you will need to create the database and user as per usual for a MySQL-backed
web application before running message-store.

# TODO

* add licence headers to all source files
* make server settings configurable by file
* make db settings configurable by file
* unit tests - see https://glebbahmutov.com/blog/how-to-correctly-unit-test-express-server/
* use SQLite in memory for unit tests
* make server settings configurable by file or env
* coverage reporting for unit tests
* add ORM, default to SQLite
* get server.startServer() to show the IP address the server is running on
* automatic server reload when code changes
* proper logging, instead of just logging to console
* instructions to install as a service on Linux

# Bugs
Expand Down
3 changes: 2 additions & 1 deletion bin/message-store
@@ -1,2 +1,3 @@
#!/usr/bin/env node
require('../src/app.js');
var path = require('path');
require(path.join(__dirname, '..', 'src', 'main'));
4 changes: 4 additions & 0 deletions config/config.json
@@ -0,0 +1,4 @@
{
"db": {
}
}
21 changes: 21 additions & 0 deletions gulpfile.js
@@ -0,0 +1,21 @@
var gulp = require('gulp');
var jshint = require('gulp-jshint');
var mocha = require('gulp-mocha');

gulp.task('lint', function() {
return gulp.src('./src/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});

gulp.task('lint-tests', function() {
return gulp.src('./test/**/*.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});

gulp.task('test-integration', function () {
return gulp.src('./test/integration/test.*.js')
// gulp-mocha needs filepaths so you can't have any plugins before it
.pipe(mocha({reporter: 'nyan'}));
});
32 changes: 32 additions & 0 deletions migrations/20160805093815-add-messages-table.js
@@ -0,0 +1,32 @@
'use strict';

module.exports = {
up: function (queryInterface, Sequelize) {
// create "messages" table
queryInterface.createTable(
'messages',
{
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
createdAt: {
type: Sequelize.DATE
},
updatedAt: {
type: Sequelize.DATE
},
text: {
type: Sequelize.STRING,
allowNull: false
}
}
);
},

down: function (queryInterface, Sequelize) {
// drop "message" table
queryInterface.dropTable('message');
}
};
18 changes: 17 additions & 1 deletion package.json
Expand Up @@ -2,6 +2,7 @@
"name": "message-store",
"version": "0.0.1",
"description": "Store simple text messages and retrieve them from a web service.",
"repository": "https://github.com/townxelliot/message-store",
"main": "./src/app.js",
"bin": "./bin/message-store",
"scripts": {
Expand All @@ -10,6 +11,21 @@
"author": "Elliot Smith <elliot@townx.org>",
"license": "MIT",
"dependencies": {
"express": "^4.14.0"
"bluebird": "^3.4.1",
"body-parser": "^1.15.2",
"express": "^4.14.0",
"nconf": "^0.8.4",
"sequelize": "^3.23.6",
"sqlite3": "^3.1.4",
"umzug": "^1.11.0"
},
"devDependencies": {
"chai": "^3.5.0",
"gulp": "^3.9.1",
"gulp-jshint": "^2.0.1",
"gulp-mocha": "^3.0.0",
"jshint": "^2.9.2",
"portfinder": "^1.0.5",
"supertest": "^2.0.0"
}
}
26 changes: 20 additions & 6 deletions src/app.js
@@ -1,9 +1,23 @@
var dbSetup = require('./db');
var startServer = require('./server');
var Promise = require('bluebird');

// TODO move to config
var PORT = 3000;
/**
* Start the message-store app on hostname:port
*
* @param {string} dbConfigFilePath - Path to the JSON configuration file
* for the database; see README.md
* @param {string} hostname - Host to run the app on
* @param {integer} port - HTTP port to run the app on
*
* @returns {Promise} - Resolves to the Express Application once the server
* has started
*/
var startApp = function (dbConfigFilePath, hostname, port) {
// TODO pass db config path to dbSetup from env
return dbSetup(dbConfigFilePath).then(function (models) {
return startServer(hostname, port, models);
});
};

// TODO make IP address configurable, defaulting to 0.0.0.0, and pass
// to startServer

startServer(PORT);
module.exports = startApp;
75 changes: 75 additions & 0 deletions src/connection.js
@@ -0,0 +1,75 @@
// database connection configuration and setup

var nconf = require('nconf');
var Sequelize = require('sequelize');

// SQLite dialect is the default
var SQLITE_DIALECT = 'sqlite';

// dialects available to Sequelize
var DIALECTS = [SQLITE_DIALECT, 'mysql', 'mssql', 'postgres'];

// string for storage option, to set up SQLite in memory
var IN_MEMORY_STORAGE = ':memory:';

// default settings for the Sequelize pool
var DEFAULT_POOL = {
max: 5,
min: 0,
idle: 10000
};

/**
* Configure and return a Sequelize instance;
* the configuration file format is shown in the README.
*
* @param {string} configFilePath - Path to JSON configuration file to load
* db configuration from
*
* @returns {Sequelize} - Configured Sequelize instance
*/
var getConnection = function (configFilePath) {
// configure from command line, then from env
nconf.argv().env();

// configure from file if supplied
if (configFilePath) {
nconf.file({file: configFilePath});
}

// default to in-memory sqlite
nconf.defaults({
dialect: SQLITE_DIALECT,
storage: IN_MEMORY_STORAGE,
pool: DEFAULT_POOL
});

// check dialect is valid
var dialect = nconf.get('dialect');
if (DIALECTS.indexOf(dialect) === -1) {
throw new Error('specified dialect ' + dialect + ' is not available; ' +
'please specify one of ' + JSON.stringify(DIALECTS));
}

// check required options
if (dialect === SQLITE_DIALECT) {
nconf.required(['storage']);
}
else {
nconf.required(['database', 'username', 'password']);
if (!(nconf.get('host') || nconf.get('socketPath'))) {
throw new Error('host (or host or socketPath for MySQL) must be ' +
'specified');
}
}

// config ok, so set up the connection
var database = nconf.get('database');
var username = nconf.get('username');
var password = nconf.get('password');
var config = nconf.get();

return new Sequelize(database, username, password, config);
};

module.exports = getConnection;

0 comments on commit 4050789

Please sign in to comment.