First commit
This commit is contained in:
commit
2403bdfcee
18
.env.example
Normal file
18
.env.example
Normal file
@ -0,0 +1,18 @@
|
||||
################################################################################
|
||||
# Environment Configuration
|
||||
################################################################################
|
||||
|
||||
NODE_ENV=development
|
||||
HOST=localhost
|
||||
|
||||
# Enable/disable protocols
|
||||
HTTP=true
|
||||
HTTPS=false
|
||||
|
||||
# Server ports
|
||||
HTTP_PORT=8000
|
||||
HTTPS_PORT=3443
|
||||
|
||||
# Certificate paths are relative to the server.js file
|
||||
HTTPS_CONFIG_KEY=localhost.key
|
||||
HTTPS_CONFIG_CERT=localhost.crt
|
73
.gitignore
vendored
Normal file
73
.gitignore
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
# Created by https://www.gitignore.io/api/node,osx,webstorm,eclipse
|
||||
|
||||
### Node ###
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
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
|
||||
.npm
|
||||
|
||||
# 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/docs/*
|
||||
public/api-docs/*
|
||||
public/coverage/*
|
||||
|
||||
# Don't commit environment file
|
||||
.env
|
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
|
||||
}
|
||||
}
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Timothy Warren
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# ProgBlog
|
||||
|
||||
A simple node blog with built-in code snippet highlighting
|
123
app/Container.js
Normal file
123
app/Container.js
Normal file
@ -0,0 +1,123 @@
|
||||
'use strict';
|
||||
|
||||
const errors = require('errors');
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const autoLoad = require('./config/container-autoload');
|
||||
|
||||
/**
|
||||
* Determine the appropriate path to a module relative to the 'app' folder
|
||||
*
|
||||
* @private
|
||||
* @param {string} modulePath - the raw path to the module
|
||||
* @return {string} - the normalized path to the module
|
||||
*/
|
||||
function 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('./') || modulePath.includes(__dirname))
|
||||
) {
|
||||
includePath = path.join(__dirname, modulePath);
|
||||
}
|
||||
|
||||
return includePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Container for keeping track of dependencies
|
||||
*/
|
||||
class Container {
|
||||
constructor() {
|
||||
const app = express();
|
||||
let container = new Map();
|
||||
|
||||
// Save the base app object
|
||||
container.set('app', app);
|
||||
|
||||
// Preload some configured modules
|
||||
autoLoad.map((module) => {
|
||||
let moduleMap = module;
|
||||
|
||||
// Normalize config into [key, value]
|
||||
if (! Array.isArray(module)) {
|
||||
moduleMap = [module, module];
|
||||
}
|
||||
|
||||
// Actually require the module
|
||||
moduleMap[1] = require(normalizeIncludePath(moduleMap[1]));
|
||||
|
||||
container.set.apply(container, moduleMap);
|
||||
});
|
||||
|
||||
this._container = container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 this._container.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an existing object instance
|
||||
*
|
||||
* @param {string} name - name of the item
|
||||
* @return {Object|undefined} - the item, or undefined if it doesn't exist
|
||||
*/
|
||||
get(name) {
|
||||
if (this.has(name)) {
|
||||
return this._container.get(name);
|
||||
}
|
||||
|
||||
try {
|
||||
return this._require(name);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.set(name, object);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a native require, relative to the lib folder,
|
||||
* and returns the value
|
||||
*
|
||||
* @private
|
||||
* @param {string} modulePath - name of the module to require
|
||||
* @return {*} - the value returned from require
|
||||
*/
|
||||
_require(modulePath) {
|
||||
// If the value is already saved, just return it
|
||||
if (this.has(modulePath)) {
|
||||
return this.get(modulePath);
|
||||
}
|
||||
|
||||
// Save the item for later
|
||||
let item = require(normalizeIncludePath(modulePath));
|
||||
this.set(modulePath, item);
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Container();
|
104
app/base/ApiModel.js
Normal file
104
app/base/ApiModel.js
Normal file
@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
const container = require('../Container'),
|
||||
axios = require('axios');
|
||||
const Model = container.get('base/Model');
|
||||
|
||||
/**
|
||||
* Base class for API Models
|
||||
*
|
||||
* Wraps convenience methods for making API requests.
|
||||
*
|
||||
* @param {string} [baseUrl] - a base url for api requests
|
||||
* @extends Model
|
||||
*/
|
||||
class ApiModel extends Model {
|
||||
/**
|
||||
* Create a new APIModel
|
||||
*
|
||||
* @constructor
|
||||
* @param {string} [baseUrl] - a base url for api requests
|
||||
*/
|
||||
constructor(baseUrl) {
|
||||
super();
|
||||
|
||||
let apiConfig = {};
|
||||
|
||||
if (baseUrl) {
|
||||
apiConfig.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
this.client = axios(apiConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'HEAD' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
head(url, config) {
|
||||
return this.client.head(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'GET' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
get(url, config) {
|
||||
return this.client.get(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'DELETE' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
delete(url, config) {
|
||||
return this.client.delete(url, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'POST' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} data - the data for the current request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
post(url, data, config) {
|
||||
return this.client.post(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'PUT' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} data - the data for the current request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
put(url, data, config) {
|
||||
return this.client.put(url, data, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a 'PATCH' HTTP request
|
||||
*
|
||||
* @param {string} url - the url to request
|
||||
* @param {Object} data - the data for the current request
|
||||
* @param {Object} [config] - options for the current request
|
||||
* @return {Promise} - promise wrapping the request
|
||||
*/
|
||||
patch(url, data, config) {
|
||||
return this.client.patch(url, data, config);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ApiModel;
|
180
app/base/Config.js
Normal file
180
app/base/Config.js
Normal file
@ -0,0 +1,180 @@
|
||||
'use strict';
|
||||
|
||||
const container = require('../Container');
|
||||
const path = container.get('path');
|
||||
|
||||
// Load environment file
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../../.env'),
|
||||
});
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
const glob = require('glob');
|
||||
const configSslPath = path.resolve(__dirname, '../config/ssl');
|
||||
const configPath = path.resolve(__dirname, '../config');
|
||||
|
||||
const defaultConfig = new Map([
|
||||
['host', 'localhost'],
|
||||
['http', true],
|
||||
['http-port', 80],
|
||||
['https', false],
|
||||
['https-port', 443],
|
||||
['node-env', 'development'],
|
||||
]);
|
||||
|
||||
const configFiles = glob.sync(`${configPath}/**/*.js`);
|
||||
|
||||
/**
|
||||
* Config management class
|
||||
*
|
||||
* Hierarchy of config options
|
||||
* 1. Directly defined config options
|
||||
* 2. Environment variables
|
||||
* 3. Default values
|
||||
*/
|
||||
class Config {
|
||||
constructor() {
|
||||
let _configMap = new Map();
|
||||
|
||||
// Load files in config folder under their
|
||||
// own respective namespaces
|
||||
configFiles.forEach((fullPath) => {
|
||||
let key = path.basename(fullPath, '.js');
|
||||
_configMap.set(key, require(fullPath));
|
||||
});
|
||||
|
||||
this._config = _configMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of available configuration keys
|
||||
*
|
||||
* @return {array} - the list of configuration keys
|
||||
*/
|
||||
keys() {
|
||||
let keys = [];
|
||||
|
||||
for (let key of this._config.keys()) {
|
||||
keys.push(key);
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a config value exists
|
||||
*
|
||||
* @param {string|symbol} key - the config value key
|
||||
* @return {boolean} - whether the config value exists
|
||||
*/
|
||||
has(key) {
|
||||
return this._config.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether an environment variable is currently defined
|
||||
*
|
||||
* @param {string} key - the environment varaible to look for
|
||||
* @return {boolean} - whether the environment variable is defined
|
||||
*/
|
||||
hasEnv(key) {
|
||||
return process.env[this._envName(key)] != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a config value
|
||||
*
|
||||
* @param {string|symbol} key - the name of the config value
|
||||
* @return {*} - the configuration value
|
||||
*/
|
||||
get(key) {
|
||||
if (! this.has(key)) {
|
||||
// Fallback to environment variables
|
||||
let envKey = key.toUpperCase().replace('-', '_');
|
||||
if (this.hasEnv(envKey)) {
|
||||
let envValue = this.getEnv(envKey);
|
||||
this._config.set(key, envValue);
|
||||
return envValue;
|
||||
}
|
||||
|
||||
// Fallback to default values
|
||||
if (defaultConfig.has(key)) {
|
||||
let defaultValue = defaultConfig.get(key);
|
||||
this._config.set(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return this._config.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of an environment variable
|
||||
*
|
||||
* @param {string} key - the environment variable to get
|
||||
* @return {string|undefined} - the value of the environment variable
|
||||
*/
|
||||
getEnv(key) {
|
||||
let raw = process.env[this._envName(key)];
|
||||
return this._normalizeValue(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get equivalent environment variable for config key
|
||||
*
|
||||
* @private
|
||||
* @param {string} key - the config key
|
||||
* @return {string} - the environment variable name
|
||||
*/
|
||||
_envName(key) {
|
||||
return key.toUpperCase().replace('-', '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to parse javascript types from strings
|
||||
*
|
||||
* @private
|
||||
* @param {string} val - the string value
|
||||
* @return {string|number|boolean} - the 'parsed' value
|
||||
*/
|
||||
_normalizeValue(val) {
|
||||
let bool = /^true|false$/;
|
||||
let integer = /^([0-9]+)$/;
|
||||
|
||||
if (String(val).search(integer) !== -1) {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
if (String(val).search(bool) !== -1) {
|
||||
switch (val) {
|
||||
case 'true':
|
||||
return true;
|
||||
|
||||
// return overrides break
|
||||
|
||||
case 'false':
|
||||
return false;
|
||||
|
||||
// return overrides break
|
||||
}
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a config variable (mainly for testing)
|
||||
*
|
||||
* @private
|
||||
* @param {mixed} key - the key to set
|
||||
* @param {mixed} val - the value for the key
|
||||
* @return {Config} - the config instance
|
||||
*/
|
||||
_set(key, val) {
|
||||
this._config.set(key, val);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Config();
|
74
app/base/Controller.js
Normal file
74
app/base/Controller.js
Normal file
@ -0,0 +1,74 @@
|
||||
'use strict';
|
||||
|
||||
const container = require('../Container');
|
||||
const express = require('express');
|
||||
const errors = require('errors');
|
||||
const getArgs = require('getargs');
|
||||
const loadRoutes = container.get('base/util/route-loader');
|
||||
const _ = container.get('_');
|
||||
|
||||
/**
|
||||
* Controller setup and utility class
|
||||
*/
|
||||
class Controller {
|
||||
|
||||
/**
|
||||
* Get route mapping
|
||||
*
|
||||
* @param {string} baseRoutePath - the path to the folder containing
|
||||
* controllers/routes
|
||||
* @return {map} - Maps route prefixes to their respective functions
|
||||
*/
|
||||
static getRouteMap(baseRoutePath) {
|
||||
const routeMap = {};
|
||||
const pathFileMap = loadRoutes(baseRoutePath);
|
||||
|
||||
_(pathFileMap).forEach((routeFile, routePath) => {
|
||||
let rawRoutes = require(routeFile);
|
||||
routeMap[routePath] = Controller._parseRoutes(rawRoutes);
|
||||
});
|
||||
|
||||
return routeMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify route generation with an object
|
||||
*
|
||||
* @private
|
||||
* @param {Object} routeObject - the object laying out the routes for the
|
||||
* current controller
|
||||
* @return {Router} - the updated express.Router object
|
||||
*/
|
||||
static _parseRoutes(routeObject) {
|
||||
const router = express.Router();
|
||||
|
||||
_(routeObject).forEach((httpMethods, currentPath) => {
|
||||
_(httpMethods).forEach((routeFunction, currentHttpMethod) => {
|
||||
let routerMethod = router[currentHttpMethod];
|
||||
return router[currentHttpMethod](currentPath, routeFunction);
|
||||
});
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a specific http error code
|
||||
*
|
||||
* @param {number} code - The error Number
|
||||
* @param {Object} [options={}] - Options, such as message and explanation
|
||||
* @param {function} next - The callback to pass the error to
|
||||
* @return {void}
|
||||
*/
|
||||
static HttpError(/*code, options, next*/) {
|
||||
let args = getArgs('code:Number, [options]:object, next:function', arguments);
|
||||
args.options = args.options || {};
|
||||
|
||||
let methodName = `Http${args.code}Error`;
|
||||
let err = new errors[methodName](args.options);
|
||||
|
||||
return args.next(err);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Controller;
|
33
app/base/HttpServer.js
Normal file
33
app/base/HttpServer.js
Normal file
@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
|
||||
const http = require('http'),
|
||||
logger = require('winston');
|
||||
|
||||
const Server = require('./Server');
|
||||
|
||||
/**
|
||||
* Class for creating an http server
|
||||
*
|
||||
* @extends Server
|
||||
* @param {Express} app - current express instance
|
||||
* @param {number} port - the port to listen on
|
||||
*/
|
||||
class HttpServer extends Server {
|
||||
constructor(app, port) {
|
||||
super();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpServer;
|
34
app/base/HttpsServer.js
Normal file
34
app/base/HttpsServer.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
const https = require('https'),
|
||||
logger = require('winston');
|
||||
|
||||
const Server = require('./Server');
|
||||
|
||||
/**
|
||||
* Class for creating an https server
|
||||
*
|
||||
* @extends Server
|
||||
* @param {Express} app - current express instance
|
||||
* @param {number} port - the port to listen on
|
||||
* @param {Object} options - https server options
|
||||
*/
|
||||
class HttpsServer extends Server {
|
||||
constructor(app, port, options) {
|
||||
super();
|
||||
let server = https.createServer(options, 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;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HttpsServer;
|
10
app/base/Model.js
Normal file
10
app/base/Model.js
Normal file
@ -0,0 +1,10 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Base Model class
|
||||
*/
|
||||
class Model {
|
||||
|
||||
}
|
||||
|
||||
module.exports = Model;
|
42
app/base/Server.js
Normal file
42
app/base/Server.js
Normal file
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Base Class for wrapping HTTP/HTTPS servers
|
||||
*/
|
||||
class Server {
|
||||
constructor() {}
|
||||
/**
|
||||
* Event listener for HTTP(s) 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 = Server;
|
30
app/base/util/route-loader.js
Normal file
30
app/base/util/route-loader.js
Normal file
@ -0,0 +1,30 @@
|
||||
'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.replace(/\\/g, '/');
|
||||
|
||||
let paths = glob.sync(`${path}/**/*.js`);
|
||||
paths = paths.sort();
|
||||
paths = paths.map((path) => path.replace('\\', '/'));
|
||||
|
||||
let routes = {};
|
||||
|
||||
paths.forEach((path) => {
|
||||
let routePath = path.replace(basePath, '')
|
||||
.replace('.js', '')
|
||||
.replace('index', '');
|
||||
|
||||
routes[routePath] = path;
|
||||
});
|
||||
|
||||
return routes;
|
||||
};
|
31
app/bootstrap.js
vendored
Normal file
31
app/bootstrap.js
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
'use strict';
|
||||
|
||||
const container = require('./Container');
|
||||
const _ = container.get('_');
|
||||
const app = container.get('app');
|
||||
const path = container.get('path');
|
||||
const Controller = container.get('base/Controller');
|
||||
const Config = container.get('base/Config');
|
||||
|
||||
module.exports = (function () {
|
||||
const baseRoutePath = path.join(__dirname, 'controllers');
|
||||
|
||||
// Set up templating
|
||||
const view = Config.get('view-engine');
|
||||
view.setup(app);
|
||||
|
||||
// load middleware
|
||||
Config.get('middleware').forEach((mw) => app.use(mw));
|
||||
|
||||
// automatically set up routing by folder structure
|
||||
let routeMap = Controller.getRouteMap(baseRoutePath);
|
||||
_(routeMap).forEach((routeFunction, routePrefix) => {
|
||||
app.use(routePrefix, routeFunction);
|
||||
});
|
||||
|
||||
// load error handlers
|
||||
Config.get('error-handlers').forEach((handler) => app.use(handler));
|
||||
|
||||
return app;
|
||||
|
||||
}());
|
15
app/config/container-autoload.js
Normal file
15
app/config/container-autoload.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* A list of modules to insert into the Container at start up
|
||||
*
|
||||
* Modules to be loaded with an alias should have an array with the alias
|
||||
* then the module name. Otherwise, just a string with the module name
|
||||
*
|
||||
* @type {array}
|
||||
*/
|
||||
module.exports = [
|
||||
['_', 'lodash'],
|
||||
'path',
|
||||
['promisify', 'helpers/promisify'],
|
||||
];
|
43
app/config/error-handlers.js
Normal file
43
app/config/error-handlers.js
Normal file
@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Error handlers
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const container = require('../Container');
|
||||
const app = container.get('app');
|
||||
const HTTP_CODE_MAP = require('http').STATUS_CODES;
|
||||
const errors = require('errors');
|
||||
|
||||
let errorHandlers = new Set([
|
||||
|
||||
function handle404(req, res, next) {
|
||||
// if no route matches, send a 404
|
||||
if (! req.route) {
|
||||
let err = new errors.Http404Error();
|
||||
return next(err);
|
||||
}
|
||||
},
|
||||
|
||||
// general error handler
|
||||
function handleError(err, req, res, next) {
|
||||
let httpStatus = err.status || 500;
|
||||
let message = err.message || HTTP_CODE_MAP[httpStatus];
|
||||
|
||||
res.status(httpStatus);
|
||||
|
||||
let output = {
|
||||
status: httpStatus,
|
||||
message: message,
|
||||
};
|
||||
|
||||
// Show stack trace in development environment
|
||||
if (app.get('env') === 'development') {
|
||||
output.error = err;
|
||||
}
|
||||
|
||||
res.json(output);
|
||||
},
|
||||
]);
|
||||
|
||||
module.exports = errorHandlers;
|
40
app/config/middleware.js
Normal file
40
app/config/middleware.js
Normal file
@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Middleware
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
const bodyParser = require('body-parser');
|
||||
const cookieParser = require('cookie-parser');
|
||||
const express = require('express');
|
||||
const helmet = require('helmet');
|
||||
const requestLogger = require('morgan');
|
||||
const path = require('path');
|
||||
const session = require('express-session');
|
||||
|
||||
let middleware = new Set([
|
||||
|
||||
// some security settings controlled by helmet
|
||||
helmet.frameguard(),
|
||||
helmet.hidePoweredBy(),
|
||||
helmet.ieNoOpen(),
|
||||
helmet.noSniff(),
|
||||
helmet.xssFilter(),
|
||||
|
||||
// session setup
|
||||
session({
|
||||
name: 'blogid',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
secret: '76al;p9vse',
|
||||
}),
|
||||
|
||||
// basic express middleware
|
||||
requestLogger('combined'),
|
||||
bodyParser.json(),
|
||||
bodyParser.urlencoded({ extended: false }),
|
||||
cookieParser(),
|
||||
express.static(path.join(__dirname, '../../public')),
|
||||
]);
|
||||
|
||||
module.exports = middleware;
|
15
app/config/view-engine.js
Normal file
15
app/config/view-engine.js
Normal file
@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
// Stupid template engine requires coffescript for some reason
|
||||
const cs = require('coffee-script');
|
||||
cs.register();
|
||||
|
||||
const path = require('path');
|
||||
const hulk = require('hulk-hogan');
|
||||
|
||||
module.exports.setup = (app) => {
|
||||
let viewPath = path.resolve(__dirname, '../views');
|
||||
app.set('views', viewPath);
|
||||
app.set('view options', { layout: false });
|
||||
app.set('view engine', 'hulk');
|
||||
};
|
20
app/controllers/index.js
Normal file
20
app/controllers/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
'/': {
|
||||
|
||||
// Get homepage
|
||||
get: (req, res) => {
|
||||
return res.json({
|
||||
status: 200,
|
||||
data: {
|
||||
index: { title: 'Express' },
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
put: (req, res, next) => {
|
||||
return next();
|
||||
},
|
||||
},
|
||||
};
|
26
app/helpers/promisify.js
Normal file
26
app/helpers/promisify.js
Normal file
@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
/*eslint-disable prefer-arrow-callback*/
|
||||
/**
|
||||
* Function to convert a callback function into a promise
|
||||
*
|
||||
* @see http://eddmann.com/posts/promisifying-error-first-asynchronous-callbacks-in-javascript/
|
||||
* @example promisify(fs.readFile)('hello.txt', 'utf8')
|
||||
* .then(console.log)
|
||||
* .catch(console.error)
|
||||
* @param {Function} fn - the callback function to convert
|
||||
* @return {Promise} - the new promise
|
||||
*/
|
||||
function promisify(fn) {
|
||||
return function () {
|
||||
let args = [].slice.call(arguments);
|
||||
return new Promise(function (resolve, reject) {
|
||||
fn.apply(undefined, args.concat((error, value) => {
|
||||
return error ? reject(error) : resolve(value);
|
||||
}));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = promisify;
|
||||
/*eslint-enable prefer-arrow-callback*/
|
245
gulpfile.js
Normal file
245
gulpfile.js
Normal file
@ -0,0 +1,245 @@
|
||||
'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 = [
|
||||
'app/base/**/*.js',
|
||||
'app/config/**/*.js',
|
||||
'app/controllers/**/*.js',
|
||||
'app/helpers/**/*.js',
|
||||
'app/models/**/*.js',
|
||||
'app/*.js',
|
||||
];
|
||||
|
||||
/*
|
||||
* Path to unit test files
|
||||
*/
|
||||
const UNIT_TEST_FILES = ['test/unit/**/*_test.js'];
|
||||
|
||||
/*
|
||||
* Path to integration test files
|
||||
*/
|
||||
const INTEGRATION_TEST_FILES = ['test/integration/**/*_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
|
||||
'no-case-declarations': [2], // Don't define variables in switch labels
|
||||
'no-const-assign': [2], // Highlight instances where assigning to const declaration
|
||||
'no-new-symbol': [2], // Symbol is not a constructor, don't use the new keyword
|
||||
'no-unused-labels': [2], // Error on labels in code that aren't used
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
* Configuration values for mocha
|
||||
*/
|
||||
const MOCHA_SETTINGS = {
|
||||
ui: 'tdd',
|
||||
bail: true,
|
||||
slow: 1000,
|
||||
timeout: 5000,
|
||||
};
|
||||
|
||||
/*
|
||||
* 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: 'app/',
|
||||
dest: 'public/api-docs/',
|
||||
}, done);
|
||||
});
|
||||
|
||||
/*
|
||||
* Run all documentation generation tasks
|
||||
*/
|
||||
gulp.task('docs', ['src-docs', 'api-docs']);
|
||||
|
||||
/*
|
||||
* Run integration tests
|
||||
*/
|
||||
gulp.task('integration-test', ['lint'], () => {
|
||||
return gulp.src(INTEGRATION_TEST_FILES)
|
||||
.pipe(mocha(MOCHA_SETTINGS));
|
||||
});
|
||||
|
||||
/*
|
||||
* Run unit tests
|
||||
*/
|
||||
gulp.task('unit-test', ['lint'], () => {
|
||||
return gulp.src(UNIT_TEST_FILES)
|
||||
.pipe(mocha(MOCHA_SETTINGS));
|
||||
});
|
||||
|
||||
/*
|
||||
* Run all tests
|
||||
*/
|
||||
gulp.task('test', ['lint'], () => {
|
||||
return gulp.src(TEST_FILES)
|
||||
.pipe(mocha(MOCHA_SETTINGS));
|
||||
});
|
||||
|
||||
/*
|
||||
* 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 integration tests
|
||||
*/
|
||||
gulp.task('coverage', ['lint', 'pre-coverage'], () => {
|
||||
return pipe(gulp.src(UNIT_TEST_FILES), [
|
||||
mocha(MOCHA_SETTINGS),
|
||||
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
|
74
package.json
Normal file
74
package.json
Normal file
@ -0,0 +1,74 @@
|
||||
{
|
||||
"author": "Timothy J. Warren",
|
||||
"dependencies": {
|
||||
"axios": "^0.9.1",
|
||||
"body-parser": "~1.13.2",
|
||||
"ci-node-query": "^3.1.0",
|
||||
"coffee-script": "^1.10.0",
|
||||
"cookie-parser": "~1.3.5",
|
||||
"debug": "~2.2.0",
|
||||
"dotenv": "^2.0.0",
|
||||
"errors": "^0.3.0",
|
||||
"eslint": "^1.10.3",
|
||||
"express": "4.*",
|
||||
"express-session": "^1.13.0",
|
||||
"getargs": "0.0.8",
|
||||
"glob": "^6.0.4",
|
||||
"helmet": "^1.1.0",
|
||||
"highlight.js": "^9.1.0",
|
||||
"hulk-hogan": "0.0.9",
|
||||
"lodash": "^4.5.0",
|
||||
"marked": "^0.3.5",
|
||||
"morgan": "~1.6.1",
|
||||
"nodemon": "^1.9.0",
|
||||
"winston": "^2.1.1"
|
||||
},
|
||||
"description": "An Opinionated Take on express with use of ES6 features",
|
||||
"devDependencies": {
|
||||
"chai": "^3.4.1",
|
||||
"chai-as-promised": "^5.2.0",
|
||||
"eslint": "1.10.*",
|
||||
"gulp": "3.9.*",
|
||||
"gulp-apidoc": "0.2.*",
|
||||
"gulp-documentation": "2.1.*",
|
||||
"gulp-eslint": "^2.0.0",
|
||||
"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": "nodemon server.js",
|
||||
"gulp": "gulp default",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"version": "0.0.1"
|
||||
}
|
0
public/.gitkeep
Normal file
0
public/.gitkeep
Normal file
51
server.js
Normal file
51
server.js
Normal file
@ -0,0 +1,51 @@
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const container = require('./app/Container');
|
||||
const app = container.get('./bootstrap');
|
||||
const config = container.get('base/Config');
|
||||
const HttpServer = container.get('base/HttpServer');
|
||||
const HttpsServer = container.get('base/HttpsServer');
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// Create HTTP Server
|
||||
if (true === config.get('http')) {
|
||||
let port = normalizePort(config.get('http-port'));
|
||||
app.set('port', port);
|
||||
container.set('http-server', new HttpServer(app, port));
|
||||
}
|
||||
|
||||
// Create HTTPs Server
|
||||
if (true === config.get('https')) {
|
||||
const httpsPort = normalizePort(config.get('https-port'));
|
||||
const httpsConfig = {
|
||||
key: fs.readFileSync(config.get('https-config-key')),
|
||||
cert: fs.readFileSync(config.get('https-config-cert')),
|
||||
};
|
||||
app.set('https-port', httpsPort);
|
||||
container.set('https-server', new HttpsServer(app, httpsPort, httpsConfig));
|
||||
}
|
||||
|
||||
// End of server.js
|
53
test/integration/index_test.js
Normal file
53
test/integration/index_test.js
Normal file
@ -0,0 +1,53 @@
|
||||
'use strict';
|
||||
|
||||
const testBase = require('../test-base');
|
||||
const expect = testBase.expect;
|
||||
const supertest = require('supertest');
|
||||
|
||||
let server = supertest.agent('http://localhost:3000');
|
||||
|
||||
suite('Example integration 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' },
|
||||
},
|
||||
});
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
test('404 page works as expected', (done) => {
|
||||
server.get('/foo')
|
||||
.expect('Content-type', /json/)
|
||||
.end((err, res) => {
|
||||
expect(res.status).to.equal(404);
|
||||
expect(res.body.status).to.equal(String(res.status));
|
||||
expect(res.body.message).to.be.ok;
|
||||
|
||||
let expected = {
|
||||
status: '404',
|
||||
message: 'Not Found',
|
||||
};
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
expected.error = {
|
||||
code: '404',
|
||||
status: '404',
|
||||
name: 'Http404Error',
|
||||
message: 'Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
expect(res.body).to.deep.equal(expected);
|
||||
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
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
|
71
test/test-base.js
Normal file
71
test/test-base.js
Normal file
@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
|
||||
// Set up chai as promised to allow for
|
||||
// better testing of promises
|
||||
const chai = require('chai');
|
||||
const chaiAsPromised = require('chai-as-promised');
|
||||
chai.use(chaiAsPromised);
|
||||
|
||||
// Load environment file
|
||||
require('dotenv').config({
|
||||
path: path.resolve(__dirname, '../.env'),
|
||||
});
|
||||
|
||||
/**
|
||||
* Base Object for unit test utilities
|
||||
*/
|
||||
const testBase = {
|
||||
|
||||
/**
|
||||
* Chai expect assertion library
|
||||
*/
|
||||
expect: 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
|
||||
*/
|
||||
require(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
|
||||
*/
|
||||
requireNoCache(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
109
test/unit/base/Config_test.js
Normal file
109
test/unit/base/Config_test.js
Normal file
@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const testBase = require('../../test-base');
|
||||
const expect = testBase.expect;
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
|
||||
suite('Config class tests', () => {
|
||||
let config = null;
|
||||
setup(() => {
|
||||
config = testBase.require('app/base/Config');
|
||||
});
|
||||
|
||||
test('Get a non-existent config value', () => {
|
||||
let actual = config.get('foo');
|
||||
expect(actual).to.be.undefined;
|
||||
});
|
||||
|
||||
suite('Value cascade', () => {
|
||||
|
||||
suite('Explicit values are loaded first', () => {
|
||||
setup(() => {
|
||||
config._set('foo', 'bar');
|
||||
});
|
||||
|
||||
test(`'foo' exists in config`, () => {
|
||||
expect(config.has('foo')).to.be.true;
|
||||
});
|
||||
test(`value of 'foo' is returned by get method`, () => {
|
||||
expect(config.get('foo')).to.be.equal('bar');
|
||||
});
|
||||
});
|
||||
|
||||
suite('Environment values are loaded before defaults', () => {
|
||||
setup(() => {
|
||||
process.env.BAR = 'baz';
|
||||
});
|
||||
|
||||
test(`'bar' is not directly defined in config`, () => {
|
||||
expect(config.has('bar')).to.be.false;
|
||||
});
|
||||
test(`'bar' is defined in environment`, () => {
|
||||
expect(config.hasEnv('bar')).to.be.true;
|
||||
expect(config.getEnv('bar')).to.be.equal(process.env.BAR);
|
||||
});
|
||||
test(`Gets 'bar' from environment`, () => {
|
||||
expect(config.get('bar')).to.be.equal(config.getEnv('bar'));
|
||||
});
|
||||
test(`'foo' is now cached in config`, () => {
|
||||
expect(config.has('bar')).to.be.true;
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
delete process.env.BAR;
|
||||
});
|
||||
});
|
||||
|
||||
suite('Default values are loaded as a last resort', () => {
|
||||
setup(() => {
|
||||
delete process.env.HOST;
|
||||
});
|
||||
|
||||
test(`'host' is not directly defined in config`, () => {
|
||||
expect(config.has('host')).to.be.false;
|
||||
});
|
||||
test(`'host' is not defined in environment`, () => {
|
||||
expect(config.hasEnv('host')).to.be.false;
|
||||
});
|
||||
test(`Get default value of 'host'`, () => {
|
||||
expect(config.get('host')).to.equal('localhost');
|
||||
});
|
||||
test(`'host' is now cached in config`, () => {
|
||||
expect(config.has('host')).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('normalizeValue', () => {
|
||||
test(`'true' returns true`, () => {
|
||||
let val = config._normalizeValue('true');
|
||||
expect(val).to.be.true;
|
||||
});
|
||||
test(`'false' returns false`, () => {
|
||||
let val = config._normalizeValue('false');
|
||||
expect(val).to.be.false;
|
||||
});
|
||||
test(`'436' returns 436`, () => {
|
||||
let val = config._normalizeValue('436');
|
||||
expect(val).to.be.equal(436);
|
||||
});
|
||||
test(`'foo' returns 'foo'`, () => {
|
||||
let val = config._normalizeValue('foo');
|
||||
expect(val).to.be.equal('foo');
|
||||
});
|
||||
});
|
||||
|
||||
test('Keys returns values in config', () => {
|
||||
// Get the list of keys for files
|
||||
let configFiles = glob.sync(
|
||||
path.resolve(__dirname, '../../../app/config/**/*.js')
|
||||
);
|
||||
let expected = configFiles.map((fullPath) => {
|
||||
return path.basename(fullPath, '.js');
|
||||
});
|
||||
let actual = config.keys();
|
||||
|
||||
expect(actual).to.be.deep.equal(expected);
|
||||
});
|
||||
});
|
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.requireNoCache('app/Container');
|
||||
});
|
||||
|
||||
test('Multiple requires return the same instance', () => {
|
||||
let container2 = testBase.require('app/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 undefined', () => {
|
||||
expect(container.get('aseiutj')).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
suite('get method require', () => {
|
||||
test('Returns same object as testInclude', () => {
|
||||
let containerFile = container.get('./bootstrap');
|
||||
let testFile = testBase.require('app/bootstrap');
|
||||
|
||||
expect(containerFile).to.be.equal(testFile);
|
||||
});
|
||||
|
||||
test('Returns same object as native require', () => {
|
||||
let containerFile = container.get('express');
|
||||
let nativeRequire = require('express');
|
||||
|
||||
expect(containerFile).to.be.equal(nativeRequire);
|
||||
});
|
||||
});
|
||||
});
|
28
test/unit/base/Controller_test.js
Normal file
28
test/unit/base/Controller_test.js
Normal file
@ -0,0 +1,28 @@
|
||||
'use strict';
|
||||
|
||||
const testBase = require('../../test-base');
|
||||
const errors = require('errors');
|
||||
const expect = testBase.expect;
|
||||
|
||||
suite('Controller tests', () => {
|
||||
let controller = null;
|
||||
|
||||
setup(() => {
|
||||
controller = testBase.requireNoCache('app/base/Controller');
|
||||
});
|
||||
|
||||
suite('HttpError Tests', () => {
|
||||
test('500 Error', (done) => {
|
||||
controller.HttpError(500, (err) => {
|
||||
expect(err).to.deep.equal(new errors.Http500Error());
|
||||
return done();
|
||||
});
|
||||
});
|
||||
test('401 Error', (done) => {
|
||||
controller.HttpError(401, (err) => {
|
||||
expect(err).to.deep.equal(new errors.Http401Error());
|
||||
return done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
25
test/unit/base/util/route-loader_test.js
Normal file
25
test/unit/base/util/route-loader_test.js
Normal file
@ -0,0 +1,25 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const testBase = require('../../../test-base');
|
||||
const expect = testBase.expect;
|
||||
|
||||
let routeLoader = testBase.require('app/base/util/route-loader');
|
||||
|
||||
function getPath(filePath) {
|
||||
return path.join(__dirname, filePath)
|
||||
.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
suite('Util - Route Loader', () => {
|
||||
test('routeLoader creates accurate route mapping', () => {
|
||||
let actual = routeLoader(path.join(__dirname, 'test-routes'));
|
||||
let expected = {
|
||||
'/api/foo/bar': getPath('test-routes/api/foo/bar.js'),
|
||||
'/api/foo': getPath('test-routes/api/foo.js'),
|
||||
'/': getPath('test-routes/index.js'),
|
||||
};
|
||||
|
||||
expect(expected).to.be.deep.equal(actual);
|
||||
});
|
||||
});
|
13
test/unit/base/util/test-routes/api/foo.js
Normal file
13
test/unit/base/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/base/util/test-routes/api/foo/bar.js
Normal file
13
test/unit/base/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/base/util/test-routes/index.js
Normal file
13
test/unit/base/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;
|
21
test/unit/helpers/promisify_test.js
Normal file
21
test/unit/helpers/promisify_test.js
Normal file
@ -0,0 +1,21 @@
|
||||
'use strict';
|
||||
|
||||
const testBase = require('../../test-base');
|
||||
const expect = testBase.expect;
|
||||
const fs = require('fs');
|
||||
const promisify = testBase.require('app/helpers/promisify');
|
||||
|
||||
suite('Promisify', () => {
|
||||
test('Promisify returns a promise', () => {
|
||||
let actual = promisify(fs.readFile)('../../test-base.js');
|
||||
expect(actual).to.be.a('Promise');
|
||||
});
|
||||
test('Promisify fs.readFile resolves', () => {
|
||||
let actual = promisify(fs.readFile)('../../test-base.js');
|
||||
expect(actual).to.be.fulfilled;
|
||||
});
|
||||
test('Promisify fs.readFile fails on non-existent file', () => {
|
||||
let actual = promisify(fs.readFile)('foo.txt');
|
||||
expect(actual).to.be.rejected;
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user