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
|
||||
*.log
|
||||
@ -24,6 +27,7 @@ coverage
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
|
||||
node_modules
|
||||
|
||||
# Optional npm cache directory
|
||||
@ -31,3 +35,34 @@ node_modules
|
||||
|
||||
# Optional 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