Add basic structure
This commit is contained in:
parent
f00de10dfb
commit
5fecd50e7c
35
.gitignore
vendored
35
.gitignore
vendored
@ -1,3 +1,6 @@
|
|||||||
|
# Created by https://www.gitignore.io/api/node,osx,webstorm,eclipse
|
||||||
|
|
||||||
|
### Node ###
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
@ -24,6 +27,7 @@ coverage
|
|||||||
build/Release
|
build/Release
|
||||||
|
|
||||||
# Dependency directory
|
# Dependency directory
|
||||||
|
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Optional npm cache directory
|
# Optional npm cache directory
|
||||||
@ -31,3 +35,34 @@ node_modules
|
|||||||
|
|
||||||
# Optional REPL history
|
# Optional REPL history
|
||||||
.node_repl_history
|
.node_repl_history
|
||||||
|
|
||||||
|
|
||||||
|
### OSX ###
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
# Don't commit generated docs
|
||||||
|
public/*
|
111
.jscsrc
Normal file
111
.jscsrc
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
{
|
||||||
|
"disallowEmptyBlocks": true,
|
||||||
|
"disallowKeywords": [
|
||||||
|
"with",
|
||||||
|
"eval"
|
||||||
|
],
|
||||||
|
"disallowKeywordsOnNewLine": [
|
||||||
|
"else"
|
||||||
|
],
|
||||||
|
"disallowMixedSpacesAndTabs": true,
|
||||||
|
"disallowMultipleLineBreaks": true,
|
||||||
|
"disallowMultipleLineStrings": true,
|
||||||
|
"disallowQuotedKeysInObjects": true,
|
||||||
|
"disallowSpaceAfterObjectKeys": true,
|
||||||
|
"disallowSpaceAfterPrefixUnaryOperators": false,
|
||||||
|
"disallowSpaceBeforeBinaryOperators": [
|
||||||
|
","
|
||||||
|
],
|
||||||
|
"disallowSpaceBeforeComma": {
|
||||||
|
"allExcept": [
|
||||||
|
"sparseArrays"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disallowSpaceBeforePostfixUnaryOperators": [
|
||||||
|
"++",
|
||||||
|
"--"
|
||||||
|
],
|
||||||
|
"disallowSpaceBeforeSemicolon": true,
|
||||||
|
"disallowSpacesInCallExpression": true,
|
||||||
|
"disallowSpacesInFunctionDeclaration": {
|
||||||
|
"beforeOpeningRoundBrace": true
|
||||||
|
},
|
||||||
|
"disallowSpacesInNamedFunctionExpression": {
|
||||||
|
"beforeOpeningRoundBrace": true
|
||||||
|
},
|
||||||
|
"disallowSpacesInsideArrayBrackets": true,
|
||||||
|
"disallowSpacesInsideBrackets": true,
|
||||||
|
"disallowSpacesInsideParentheses": {
|
||||||
|
"only": [
|
||||||
|
"!",
|
||||||
|
"{",
|
||||||
|
"}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"disallowTrailingWhitespace": true,
|
||||||
|
"disallowYodaConditions": true,
|
||||||
|
"esnext": true,
|
||||||
|
"requireBlocksOnNewline": 1,
|
||||||
|
"requireCamelCaseOrUpperCaseIdentifiers": true,
|
||||||
|
"requireCapitalizedConstructors": true,
|
||||||
|
"requireCommaBeforeLineBreak": true,
|
||||||
|
"requireCurlyBraces": [
|
||||||
|
"if",
|
||||||
|
"else",
|
||||||
|
"for",
|
||||||
|
"while",
|
||||||
|
"do",
|
||||||
|
"try",
|
||||||
|
"catch"
|
||||||
|
],
|
||||||
|
"requireDotNotation": {
|
||||||
|
"allExcept": [
|
||||||
|
"keywords"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requireLineFeedAtFileEnd": false,
|
||||||
|
"requirePaddingNewLinesAfterBlocks": true,
|
||||||
|
"requirePaddingNewLinesBeforeLineComments": {
|
||||||
|
"allExcept": "firstAfterCurly"
|
||||||
|
},
|
||||||
|
"requireParenthesesAroundIIFE": true,
|
||||||
|
"requireSemicolons": true,
|
||||||
|
"requireSpaceAfterBinaryOperators": true,
|
||||||
|
"requireSpaceAfterComma": true,
|
||||||
|
"requireSpaceAfterKeywords": [
|
||||||
|
"if",
|
||||||
|
"else",
|
||||||
|
"for",
|
||||||
|
"while",
|
||||||
|
"do",
|
||||||
|
"switch",
|
||||||
|
"case",
|
||||||
|
"return",
|
||||||
|
"try",
|
||||||
|
"catch",
|
||||||
|
"typeof"
|
||||||
|
],
|
||||||
|
"requireSpaceBeforeBinaryOperators": true,
|
||||||
|
"requireSpaceBeforeBlockStatements": true,
|
||||||
|
"requireSpaceBetweenArguments": true,
|
||||||
|
"requireSpacesInAnonymousFunctionExpression": {
|
||||||
|
"beforeOpeningRoundBrace": true,
|
||||||
|
"allExcept": [
|
||||||
|
"shorthand"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"requireSpacesInConditionalExpression": true,
|
||||||
|
"requireSpacesInForStatement": true,
|
||||||
|
"requireSpacesInsideObjectBrackets": "all",
|
||||||
|
"requireTrailingComma": {
|
||||||
|
"ignoreSingleLine": true
|
||||||
|
},
|
||||||
|
"safeContextKeyword": "_this",
|
||||||
|
"validateIndentation": "\t",
|
||||||
|
"validateLineBreaks": "LF",
|
||||||
|
"validateQuoteMarks": {
|
||||||
|
"mark": "'",
|
||||||
|
"escape": true,
|
||||||
|
"ignoreJSX": true
|
||||||
|
}
|
||||||
|
}
|
219
gulpfile.js
Normal file
219
gulpfile.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const apidoc = require('gulp-apidoc'),
|
||||||
|
documentation = require('gulp-documentation'),
|
||||||
|
eslint = require('gulp-eslint'),
|
||||||
|
gulp = require('gulp'),
|
||||||
|
istanbul = require('gulp-istanbul'),
|
||||||
|
jscs = require('gulp-jscs'),
|
||||||
|
mocha = require('gulp-mocha'),
|
||||||
|
pipe = require('gulp-pipe'),
|
||||||
|
nsp = require('gulp-nsp');
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path(s) to all source files
|
||||||
|
*/
|
||||||
|
const SRC_FILES = ['lib/**/*.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to unit test files
|
||||||
|
*/
|
||||||
|
const UNIT_TEST_FILES = ['test/unit/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path to integration test files
|
||||||
|
*/
|
||||||
|
const INTEGRATION_TEST_FILES = ['test/end-to-end/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Path(s) to all test files
|
||||||
|
*/
|
||||||
|
const TEST_FILES = ['test/**/*_test.js'];
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Configuration values for eslint
|
||||||
|
*/
|
||||||
|
const ESLINT_SETTINGS = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es6: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Each rule has an error level (0-2), and some have extra parameters
|
||||||
|
// 0 turns a rule off
|
||||||
|
// 1 makes a rule give a warning
|
||||||
|
// 2 makes a rule fail linting
|
||||||
|
rules: {
|
||||||
|
'linebreak-style': [2, 'unix'], // Only unix line endings
|
||||||
|
'arrow-parens': [2, 'always'], // No parens on arrow functions with one param
|
||||||
|
'no-console': [1], // Avoid using console methods
|
||||||
|
'no-constant-condition': [1],
|
||||||
|
'no-extra-semi': [1], // Enliminate extra semicolons
|
||||||
|
'no-func-assign': [1],
|
||||||
|
'no-obj-calls': [2],
|
||||||
|
'no-unexpected-multiline': [2], // Catch syntax errors due to automatic semicolon insertion
|
||||||
|
'no-unneeded-ternary': [2], // Avoid redundant ternary expressions
|
||||||
|
radix: [2], // Require radix parameter on parseInt
|
||||||
|
'no-with': [2], // No use of with construct
|
||||||
|
'no-eval': [2], // No use of eval
|
||||||
|
'no-unreachable': [1], // Avoid code that is not reachable
|
||||||
|
'no-irregular-whitespace': [1], // Highlight whitespace that isn't a tab or space
|
||||||
|
'no-new-wrappers': [2], // Avoid using primitive constructors
|
||||||
|
'no-new-func': [2], // Avoid Function constructor eval
|
||||||
|
curly: [2, 'multi-line'], // Require braces for if,for,while,do contructs that are not on the same line
|
||||||
|
'no-implied-eval': [2], // Avoid camoflauged eval
|
||||||
|
'no-invalid-this': [2],
|
||||||
|
'constructor-super': [2],
|
||||||
|
'no-dupe-args': [2], // Disallow functions to have more than one parameter with the same name
|
||||||
|
'no-dupe-keys': [2], // Disaalow objects to have more than one property with the same name
|
||||||
|
'no-dupe-class-members': [2], // Disallow classes to have more than one method/memeber with the same name
|
||||||
|
'no-this-before-super': [2],
|
||||||
|
'prefer-arrow-callback': [1], // Prefer arrow functions for callbacks
|
||||||
|
'no-var': [2], // Use let or const instead of var
|
||||||
|
'valid-jsdoc': [1],
|
||||||
|
semi: [2, 'always'], // Require use of semicolons
|
||||||
|
strict: [2, 'global'], // have a global 'use strict' in every code file
|
||||||
|
'callback-return': [1], // return when invoking a callback
|
||||||
|
'object-shorthand': [1, 'methods'], // Prefer shorthand for functions in object literals/classes, but avoid property shorthand
|
||||||
|
'prefer-template': [1], // Prefer template strings eg. `Hello ${name}`, to string concatenation
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check syntax and style of test/miscellaneous files
|
||||||
|
*/
|
||||||
|
gulp.task('lint-tests', () => {
|
||||||
|
const LINT_TESTS_FILES = TEST_FILES.concat([
|
||||||
|
'gulpfile.js',
|
||||||
|
'server.js',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// eslint
|
||||||
|
pipe(gulp.src(LINT_TESTS_FILES), [
|
||||||
|
eslint(ESLINT_SETTINGS),
|
||||||
|
eslint.format(),
|
||||||
|
eslint.failAfterError(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// JSCS rules are defined in /.jscsrc
|
||||||
|
pipe(gulp.src(['test/**/*.js', 'gulpfile.js']), [
|
||||||
|
jscs(),
|
||||||
|
jscs.reporter(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check syntax and style of source files
|
||||||
|
*/
|
||||||
|
gulp.task('lint-src', () => {
|
||||||
|
// eslint
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
eslint(ESLINT_SETTINGS),
|
||||||
|
eslint.format(),
|
||||||
|
eslint.failAfterError(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// JSCS
|
||||||
|
// JSCS rules are defined in /.jscsrc
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
jscs(),
|
||||||
|
jscs.reporter(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all lint tasks
|
||||||
|
*/
|
||||||
|
gulp.task('lint', ['lint-src', 'lint-tests']);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create internal method documentation of source files
|
||||||
|
*/
|
||||||
|
gulp.task('src-docs', () => {
|
||||||
|
pipe(gulp.src(SRC_FILES), [
|
||||||
|
documentation({
|
||||||
|
format: 'html',
|
||||||
|
}),
|
||||||
|
gulp.dest('public/docs'),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Create api documentation from source files
|
||||||
|
*/
|
||||||
|
gulp.task('api-docs', (done) => {
|
||||||
|
apidoc({
|
||||||
|
src: 'lib/',
|
||||||
|
dest: 'public/api-docs/',
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all documentation generation tasks
|
||||||
|
*/
|
||||||
|
gulp.task('docs', ['src-docs', 'api-docs']);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all tests
|
||||||
|
*/
|
||||||
|
gulp.task('test', ['lint'], () => {
|
||||||
|
return pipe(gulp.src(TEST_FILES), [
|
||||||
|
mocha({
|
||||||
|
ui: 'tdd',
|
||||||
|
bail: true,
|
||||||
|
slow: 1000,
|
||||||
|
timeout: 5000,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run hooks for istanbul coverage generation
|
||||||
|
*/
|
||||||
|
gulp.task('pre-coverage', () => {
|
||||||
|
return pipe(gulp.src(SRC_FILES), [
|
||||||
|
istanbul(),
|
||||||
|
istanbul.hookRequire(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run unit tests and generate code coverage
|
||||||
|
*
|
||||||
|
* Does not run end-to-end tests
|
||||||
|
*/
|
||||||
|
gulp.task('coverage', ['lint', 'pre-coverage'], () => {
|
||||||
|
return pipe(gulp.src(UNIT_TEST_FILES), [
|
||||||
|
mocha({
|
||||||
|
ui: 'tdd',
|
||||||
|
bail: true,
|
||||||
|
slow: 1000,
|
||||||
|
timeout: 5000,
|
||||||
|
}),
|
||||||
|
istanbul.writeReports({
|
||||||
|
dir: 'public/coverage',
|
||||||
|
reporters:['lcov', 'lcovonly', 'html', 'text'],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Check dependencies for known security vulnerabilites
|
||||||
|
*/
|
||||||
|
gulp.task('audit', (cb) => {
|
||||||
|
nsp({
|
||||||
|
package: `${__dirname}/package.json`,
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Run all tasks
|
||||||
|
*/
|
||||||
|
gulp.task('default', [
|
||||||
|
'audit',
|
||||||
|
'lint',
|
||||||
|
'docs',
|
||||||
|
'coverage',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// End of gulpfile.js
|
96
lib/Container.js
Normal file
96
lib/Container.js
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express'),
|
||||||
|
path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Container for keeping track of dependencies
|
||||||
|
*/
|
||||||
|
class Container {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the object container
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
const app = express();
|
||||||
|
this._container = {
|
||||||
|
app: app,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if an item exists in the container
|
||||||
|
*
|
||||||
|
* @param {String} name - name of the item
|
||||||
|
* @return {Boolean} - whether the item exists in the container
|
||||||
|
*/
|
||||||
|
has(name) {
|
||||||
|
return (name in this._container);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an existing object instance
|
||||||
|
*
|
||||||
|
* @param {String} name - name of the item
|
||||||
|
* @return {Object|Null} - the item, or null if it doesn't exist
|
||||||
|
*/
|
||||||
|
get(name) {
|
||||||
|
if (! this.has(name)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._container[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an object in the container
|
||||||
|
*
|
||||||
|
* @param {String} name - name to associate with object
|
||||||
|
* @param {Object} object - item to keep track of
|
||||||
|
* @return {Container} - the container instance
|
||||||
|
*/
|
||||||
|
set(name, object) {
|
||||||
|
this._container[name] = object;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does a native require, relative to the lib folder,
|
||||||
|
* and returns the value
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - name of the module to require
|
||||||
|
* @return {mixed} - the value returned from require
|
||||||
|
*/
|
||||||
|
require(modulePath) {
|
||||||
|
// If the value is already saved, just return it
|
||||||
|
if (this.has(modulePath)) {
|
||||||
|
return this.get(modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
let includeName = modulePath;
|
||||||
|
|
||||||
|
// Allow referencing local modules without using a ./
|
||||||
|
// eg. util/route-loader instead of ./util/route-loader
|
||||||
|
if (
|
||||||
|
modulePath.includes('/') &&
|
||||||
|
! (modulePath.startsWith('./') || modulePath.includes(__dirname))
|
||||||
|
) {
|
||||||
|
includeName = path.join(__dirname, modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return require(includeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let instance = null;
|
||||||
|
|
||||||
|
module.exports = (function () {
|
||||||
|
|
||||||
|
if (instance === null) {
|
||||||
|
instance = new Container();
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}());
|
23
lib/app.js
Normal file
23
lib/app.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('./Container');
|
||||||
|
const loadRoutes = container.require('util/route-loader'),
|
||||||
|
path = container.require('path');
|
||||||
|
|
||||||
|
const app = container.get('app');
|
||||||
|
const middleware = container.require('config/middleware');
|
||||||
|
const routes = loadRoutes(path.join(__dirname, 'routes'));
|
||||||
|
const errorHandlers = container.require('config/error-handlers');
|
||||||
|
|
||||||
|
// load middleware
|
||||||
|
middleware.forEach((mw) => app.use(mw));
|
||||||
|
|
||||||
|
// automatically set up routing by folder structure
|
||||||
|
Object.keys(routes).reverse().forEach((path) => {
|
||||||
|
app.use(path, require(routes[path]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// load error handlers
|
||||||
|
errorHandlers.forEach((handler) => app.use(handler));
|
||||||
|
|
||||||
|
module.exports = app;
|
10
lib/base/ApiModel.js
Normal file
10
lib/base/ApiModel.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('../Container');
|
||||||
|
const Model = container.require('base/Model');
|
||||||
|
|
||||||
|
class ApiModel extends Model {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiModel;
|
32
lib/base/Config.js
Normal file
32
lib/base/Config.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
let nconf = require('nconf');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Config management class
|
||||||
|
*/
|
||||||
|
class Config {
|
||||||
|
constructor() {
|
||||||
|
// Use, in order of preference:
|
||||||
|
// - command line arguments
|
||||||
|
// - environment variables
|
||||||
|
// - config files
|
||||||
|
nconf.argv()
|
||||||
|
.env()
|
||||||
|
.file({
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retreive a config value
|
||||||
|
*
|
||||||
|
* @param {String} key - the name of the config value
|
||||||
|
* @return {mixed} - the configuration value
|
||||||
|
*/
|
||||||
|
get(key) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Config;
|
69
lib/base/HttpServer.js
Normal file
69
lib/base/HttpServer.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const http = require('http'),
|
||||||
|
logger = require('winston');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for creating an http server
|
||||||
|
*
|
||||||
|
* @param {Express} app - current express instance
|
||||||
|
* @param {Number} port - the port to listen on
|
||||||
|
*/
|
||||||
|
class HttpServer {
|
||||||
|
/**
|
||||||
|
* Creates an HTTP Server
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Express} app - current express instance
|
||||||
|
* @param {Number} port - the port to listen on
|
||||||
|
*/
|
||||||
|
constructor(app, port) {
|
||||||
|
let server = http.createServer(app);
|
||||||
|
server.listen(port);
|
||||||
|
server.on('error', this.onError);
|
||||||
|
server.on('listening', () => {
|
||||||
|
let addr = server.address();
|
||||||
|
let bind = typeof addr === 'string'
|
||||||
|
? `pipe ${addr}`
|
||||||
|
: `port ${addr.port}`;
|
||||||
|
logger.info(`Listening on ${bind}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.server = server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listener for HTTP server "error" event.
|
||||||
|
*
|
||||||
|
* @param {Error} error - the error object
|
||||||
|
* @return {Null} - Does not return a value
|
||||||
|
* @throws {Error}
|
||||||
|
*/
|
||||||
|
onError(error) {
|
||||||
|
if (error.syscall !== 'listen') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let port = this.server.address().port;
|
||||||
|
|
||||||
|
let bind = typeof port === 'string'
|
||||||
|
? `Pipe ${port}`
|
||||||
|
: `Port ${port}`;
|
||||||
|
|
||||||
|
// handle specific listen errors with friendly messages
|
||||||
|
switch (error.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
logger.error(`${bind} requires elevated privileges`);
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
logger.error(`${bind} is already in use`);
|
||||||
|
process.exit(1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = HttpServer;
|
7
lib/base/Model.js
Normal file
7
lib/base/Model.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
class Model {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Model;
|
37
lib/config/error-handlers.js
Normal file
37
lib/config/error-handlers.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Error handlers
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const container = require('../Container');
|
||||||
|
const app = container.get('app');
|
||||||
|
|
||||||
|
let errorHandlers = [
|
||||||
|
|
||||||
|
// catch 404 and forward to error handler
|
||||||
|
(req, res, next) => {
|
||||||
|
let err = new Error('Not Found');
|
||||||
|
err.status = 404;
|
||||||
|
next(err);
|
||||||
|
},
|
||||||
|
|
||||||
|
// general error handler
|
||||||
|
(err, req, res, next) => {
|
||||||
|
res.status(err.status || 500);
|
||||||
|
|
||||||
|
let output = {
|
||||||
|
status: err.status,
|
||||||
|
message: err.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Show stack trace in development environment
|
||||||
|
if (app.get('env') === 'development') {
|
||||||
|
output.error = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(output);
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = errorHandlers;
|
31
lib/config/middleware.js
Normal file
31
lib/config/middleware.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Middleware
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const bodyParser = require('body-parser'),
|
||||||
|
cookieParser = require('cookie-parser'),
|
||||||
|
express = require('express'),
|
||||||
|
helmet = require('helmet'),
|
||||||
|
requestLogger = require('morgan'),
|
||||||
|
path = require('path');
|
||||||
|
|
||||||
|
let middleware = [
|
||||||
|
|
||||||
|
// some security settings controlled by helmet
|
||||||
|
helmet.frameguard(),
|
||||||
|
helmet.hidePoweredBy(),
|
||||||
|
helmet.ieNoOpen(),
|
||||||
|
helmet.noSniff(),
|
||||||
|
helmet.xssFilter(),
|
||||||
|
|
||||||
|
// basic express middleware
|
||||||
|
requestLogger('combined'),
|
||||||
|
bodyParser.json(),
|
||||||
|
bodyParser.urlencoded({ extended: false }),
|
||||||
|
cookieParser(),
|
||||||
|
express.static(path.join(__dirname, '../../public')),
|
||||||
|
];
|
||||||
|
|
||||||
|
module.exports = middleware;
|
16
lib/routes/index.js
Normal file
16
lib/routes/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
index: { title: 'Express' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
11
lib/routes/users.js
Normal file
11
lib/routes/users.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET users listing. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.send('respond with a resource');
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
29
lib/util/route-loader.js
Normal file
29
lib/util/route-loader.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const glob = require('glob');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map Routes to route files
|
||||||
|
*
|
||||||
|
* @param {String} path - folder with Routes
|
||||||
|
* @return {Object} - Object mapping routes to their files
|
||||||
|
*/
|
||||||
|
module.exports = function routeLoader(path) {
|
||||||
|
|
||||||
|
const basePath = path;
|
||||||
|
|
||||||
|
let paths = glob.sync(`${path}/**/*.js`);
|
||||||
|
paths = paths.sort();
|
||||||
|
|
||||||
|
let routes = {};
|
||||||
|
|
||||||
|
paths.forEach((path) => {
|
||||||
|
let routePath = path.replace(basePath, '')
|
||||||
|
.replace('.js', '')
|
||||||
|
.replace('index', '');
|
||||||
|
|
||||||
|
routes[routePath] = path;
|
||||||
|
});
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
};
|
63
package.json
Normal file
63
package.json
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
{
|
||||||
|
"author": "Timothy J. Warren",
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": "^3.1.1",
|
||||||
|
"body-parser": "~1.13.2",
|
||||||
|
"cookie-parser": "~1.3.5",
|
||||||
|
"debug": "~2.2.0",
|
||||||
|
"express": "4.*",
|
||||||
|
"glob": "^6.0.4",
|
||||||
|
"helmet": "^1.1.0",
|
||||||
|
"morgan": "~1.6.1",
|
||||||
|
"nconf": "^0.8.2",
|
||||||
|
"node-dev": "^2.7.1",
|
||||||
|
"winston": "^2.1.1"
|
||||||
|
},
|
||||||
|
"description": "An Opinionated Take on express with use of ES6 features",
|
||||||
|
"devDependencies": {
|
||||||
|
"chai": "3.4.*",
|
||||||
|
"eslint": "1.10.*",
|
||||||
|
"gulp": "3.9.*",
|
||||||
|
"gulp-apidoc": "0.2.*",
|
||||||
|
"gulp-documentation": "2.1.*",
|
||||||
|
"gulp-eslint": "1.1.*",
|
||||||
|
"gulp-istanbul": "^0.10.3",
|
||||||
|
"gulp-jscs": "3.0.*",
|
||||||
|
"gulp-mocha": "2.2.*",
|
||||||
|
"gulp-nsp": "^2.3.0",
|
||||||
|
"gulp-pipe": "1.0.*",
|
||||||
|
"istanbul": "0.4.*",
|
||||||
|
"mocha": "2.3.*",
|
||||||
|
"pre-commit": "^1.1.2",
|
||||||
|
"supertest": "^1.1.0"
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"lib": "./lib"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">4.0.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib/",
|
||||||
|
"server.js"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./server.js",
|
||||||
|
"name": "crispy-train",
|
||||||
|
"pre-commit": {
|
||||||
|
"silent": false,
|
||||||
|
"run": [
|
||||||
|
"gulp"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/timw4mail/crispy-train.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "node-dev server.js",
|
||||||
|
"gulp": "gulp default",
|
||||||
|
"test": "gulp test"
|
||||||
|
},
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
37
server.js
Normal file
37
server.js
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const container = require('./lib/Container');
|
||||||
|
const app = container.require('./app'),
|
||||||
|
logger = require('winston'),
|
||||||
|
HttpServer = container.require('base/HttpServer');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a port into a number, string, or false.
|
||||||
|
*
|
||||||
|
* @private
|
||||||
|
* @param {mixed} val - port value
|
||||||
|
* @return {mixed} - normalized value
|
||||||
|
*/
|
||||||
|
function normalizePort(val) {
|
||||||
|
let port = parseInt(val, 10);
|
||||||
|
|
||||||
|
if (isNaN(port)) {
|
||||||
|
// named pipe
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port >= 0) {
|
||||||
|
// port number
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Get port from environment and store in Express.
|
||||||
|
let port = normalizePort(process.env.PORT || '3000');
|
||||||
|
app.set('port', port);
|
||||||
|
|
||||||
|
// Create HTTP Server
|
||||||
|
let server = new HttpServer(app, port);
|
51
test/end-to-end/index_test.js
Normal file
51
test/end-to-end/index_test.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const supertest = require('supertest'),
|
||||||
|
expect = require('chai').expect;
|
||||||
|
|
||||||
|
let server = supertest.agent('http://localhost:3000');
|
||||||
|
|
||||||
|
suite('Example tests', () => {
|
||||||
|
test('Index page works as expected', (done) => {
|
||||||
|
server.get('/')
|
||||||
|
.expect('Content-type', /json/)
|
||||||
|
.expect(200)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(200);
|
||||||
|
expect(res.body.error).to.be.undefined;
|
||||||
|
expect(res.body).to.deep.equal({
|
||||||
|
status: 200,
|
||||||
|
data: {
|
||||||
|
index: { title: 'Express' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('404 page works as expected', (done) => {
|
||||||
|
server.get('/foo')
|
||||||
|
.expect('Content-type', /json/)
|
||||||
|
.expect(404)
|
||||||
|
.end((err, res) => {
|
||||||
|
expect(res.status).to.equal(404);
|
||||||
|
expect(res.body.status).to.equal(res.status);
|
||||||
|
expect(res.body.message).to.be.ok;
|
||||||
|
|
||||||
|
let expected = {
|
||||||
|
status: 404,
|
||||||
|
message: 'Not Found',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
expected.error = {
|
||||||
|
status: 404,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(res.body).to.deep.equal(expected);
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
5
test/mocha.opts
Normal file
5
test/mocha.opts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
test/**/*.js
|
||||||
|
--ui tdd
|
||||||
|
--reporter spec
|
||||||
|
--slow 1000
|
||||||
|
--timeout 5000
|
60
test/test-base.js
Normal file
60
test/test-base.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base Object for unit test utilities
|
||||||
|
*/
|
||||||
|
const testBase = {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Chai expect assertion library
|
||||||
|
*/
|
||||||
|
expect: require('chai').expect,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the appropriate path to a module relative to the root folder
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - the raw path to the module
|
||||||
|
* @return {String} - the normalized path to the module
|
||||||
|
*/
|
||||||
|
_normalizeIncludePath(modulePath) {
|
||||||
|
const basePath = path.resolve(path.join(__dirname, '../'));
|
||||||
|
|
||||||
|
let includePath = modulePath;
|
||||||
|
|
||||||
|
// Allow referencing local modules without using a ./
|
||||||
|
// eg. util/route-loader instead of ./util/route-loader
|
||||||
|
if (modulePath.includes('/') && ! modulePath.startsWith('./')) {
|
||||||
|
includePath = path.join(basePath, modulePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return includePath;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a module relative to the root folder
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - path to the module, relative to the tests/ folder
|
||||||
|
* @return {mixed} - whatever the module returns
|
||||||
|
*/
|
||||||
|
testRequire(modulePath) {
|
||||||
|
const includePath = testBase._normalizeIncludePath(modulePath);
|
||||||
|
return require(includePath);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a module relative to the root folder, but first delete
|
||||||
|
* the module from the require cache
|
||||||
|
*
|
||||||
|
* @param {String} modulePath - path to the module, relative to the tests/ folder
|
||||||
|
* @return {mixed} - whatever the module returns
|
||||||
|
*/
|
||||||
|
testRequireNoCache(modulePath) {
|
||||||
|
const includePath = testBase._normalizeIncludePath(modulePath);
|
||||||
|
delete require.cache[includePath];
|
||||||
|
return require(includePath);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = testBase;
|
0
test/unit/.gitkeep
Normal file
0
test/unit/.gitkeep
Normal file
77
test/unit/base/Container_test.js
Normal file
77
test/unit/base/Container_test.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const testBase = require('../../test-base');
|
||||||
|
const expect = testBase.expect;
|
||||||
|
|
||||||
|
suite('Dependency Container tests', () => {
|
||||||
|
let container = null;
|
||||||
|
|
||||||
|
setup(() => {
|
||||||
|
// Delete cached version of container class that may have been required
|
||||||
|
// at an earlier time. The container module has a saved state to keep track
|
||||||
|
// of modules during normal use. Removing the cached version ensures that
|
||||||
|
// a new instance of the module is used.
|
||||||
|
container = testBase.testRequireNoCache('lib/Container');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Multiple requires return the same instance', () => {
|
||||||
|
let container2 = testBase.testRequire('lib/Container');
|
||||||
|
expect(container2).to.be.equal(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('has method', () => {
|
||||||
|
setup(() => {
|
||||||
|
container.set('foobar', {
|
||||||
|
foo: {
|
||||||
|
bar: [1, 2, 3],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item "foobar" exists', () => {
|
||||||
|
expect(container.has('foobar')).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Item "abc" does not exist', () => {
|
||||||
|
expect(container.has('abc')).to.be.false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('Get/set functionality', () => {
|
||||||
|
let obj = {
|
||||||
|
foo: {
|
||||||
|
bar: [1, 2, 3],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
test('Set method returns Container', () => {
|
||||||
|
let actual = container.set('foobar', obj);
|
||||||
|
expect(actual).to.be.equal(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Get method returns set object', () => {
|
||||||
|
let actual = container.get('foobar');
|
||||||
|
expect(actual).to.be.equal(obj);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Attempt to get non-existent item returns null', () => {
|
||||||
|
expect(container.get('aseiutj')).to.be.null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
suite('require method', () => {
|
||||||
|
test('Returns same object as testInclude', () => {
|
||||||
|
let containerFile = container.require('app');
|
||||||
|
let testFile = testBase.testRequire('lib/app');
|
||||||
|
|
||||||
|
expect(containerFile).to.be.equal(testFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Returns same object as native require', () => {
|
||||||
|
let containerFile = container.require('express');
|
||||||
|
let nativeRequire = require('express');
|
||||||
|
|
||||||
|
expect(containerFile).to.be.equal(nativeRequire);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
18
test/unit/util/route-loader_test.js
Normal file
18
test/unit/util/route-loader_test.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const expect = require('chai').expect,
|
||||||
|
path = require('path');
|
||||||
|
let routeLoader = require(path.join(__dirname, '/../../../lib/util/route-loader'));
|
||||||
|
|
||||||
|
suite('Util - Route Loader', () => {
|
||||||
|
test('routeLoader creates accurate route mapping', () => {
|
||||||
|
let actual = routeLoader(path.join(__dirname, 'test-routes'));
|
||||||
|
let expected = {
|
||||||
|
'/api/foo/bar': path.join(__dirname, 'test-routes/api/foo/bar.js'),
|
||||||
|
'/api/foo': path.join(__dirname, 'test-routes/api/foo.js'),
|
||||||
|
'/': path.join(__dirname, 'test-routes/index.js'),
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(expected).to.be.deep.equal(actual);
|
||||||
|
});
|
||||||
|
});
|
13
test/unit/util/test-routes/api/foo.js
Normal file
13
test/unit/util/test-routes/api/foo.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
13
test/unit/util/test-routes/api/foo/bar.js
Normal file
13
test/unit/util/test-routes/api/foo/bar.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
13
test/unit/util/test-routes/index.js
Normal file
13
test/unit/util/test-routes/index.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
let router = express.Router();
|
||||||
|
|
||||||
|
/* GET home page. */
|
||||||
|
router.get('/', (req, res, next) => {
|
||||||
|
res.json({
|
||||||
|
index: { title: 'Express' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
Reference in New Issue
Block a user