const FSLibrary = require('fs');
const _Extensions = require('./../resources/extensions');
const {
Server,
IncomingMessage,
ServerResponse
} = require('http');
const { Duplex } = require('stream');
const LoggerBuilder = require('./logger');
const _EmailBuilder = require('./email');
const SGAppsServerRequest = require('./server/request');
const SGAppsServerResponse = require('./server/response');
const SGAppsServerDictionary = require('./dictionary');
/**
* @class
* @name SGAppsServerDecoratorsLibrary
*/
/**
* @callback SGAppsServerErrorCallBack
* @param {Error} err
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {SGAppsServer} server
*/
/**
* @callback SGAppsServerErrorOnlyCallback
* @param {Error} err
*/
/**
* @class
* @name FSLibrary
* @mixes FSLibraryModule
*/
/**
* @class
* @name SGAppsServerShared
*/;
/**
* @callback SGAppsServerDecorator
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {SGAppsServer} server
* @param {function} callback
*/;
const _decorators = [
require('./server/extend/request-url'),
require('./server/extend/response-send'),
require('./server/extend/response-error'),
require('./server/extend/response-redirect'),
require('./server/extend/response-pipe-file'),
require('./server/extend/response-template'),
require('./server/extend/request-postdata'),
require('./server/extend/request-cookie'),
require('./server/extend/request-session'),
require('./server/extend/response-pipe-file-static'),
];
/**
* @typedef {string|RegExp} RequestPathStructure
*/
/**
* @callback RequestHandler
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {function} next
*/
/**
* @typedef {object} SGAppsServerOptions
* @property {Server} [server]
* @property {boolean} [strictRouting=false]
* @property {number} [_DEBUG_MAX_HANDLER_EXECUTION_TIME=500]
* @property {boolean} [_DEBUG_REQUEST_HANDLERS_STATS=false]
* @property {boolean} [_REQUEST_FORM_PARAMS_DEEP_PARSE=true] parse formData field names to create deep object request.body
*/
/**
* @example
* // ================================
* // Start your ๐ Web-Server app
* // ================================
*
* const { SGAppsServer } = require('@sgapps.io/server');
* const app = new SGAppsServer();
*
* app.get('/', function (req, res) {
* res.send('hello world')
* })
*
* app.server().listen(8080, () => {
* app.logger.log('Server is running on port 8080');
* })
*
* @example
* // ========================================
* // Start your ๐ Web-Server app Extended
* // ========================================
*
* const { SGAppsServer } = require('@sgapps.io/server');
* const app = new SGAppsServer();
*
* app.get('/', function (req, res) {
* res.send('hello world')
* })
*
* app.whenReady.then(() => {
* app.SessionManager.cookie = 'ssid';
* app.SessionManager.SESSION_LIFE = 120; // seconds
*
* app.server().listen(8080, () => {
* app.logger.log('Server is running on port 8080');
* })
* }, app.logger.error);
*
* @class
* @description HTTP Server for high performance results
* @name SGAppsServer
* @param {object} [options]
* @param {Server} [options.server]
* @param {boolean} [options.strictRouting=true]
* @param {boolean} [options.debug=true]
* @param {object} [options._DEBUG_MAX_HANDLER_EXECUTION_TIME=500] console shows an warn if handler is executed more than ( works in debug mode )
* @param {object} [options._DEBUG_REQUEST_HANDLERS_STATS=false] console shows an warn if handler is executed more than ( works in debug mode )
* @param {boolean} [options._REQUEST_FORM_PARAMS_DEEP_PARSE=true] parse formData field names to create deep object request.body
* @param {SGAppsServerDecorator[]} [options.decorators]
*/
function SGAppsServer(options) {
options = Object.assign(
{
server: null,
strictRouting: true,
decorators: [],
debug: true,
_DEBUG_MAX_HANDLER_EXECUTION_TIME: 500,
_DEBUG_REQUEST_HANDLERS_STATS: false,
_REQUEST_FORM_PARAMS_DEEP_PARSE: true
}, options || {}
);
/**
* @memberof SGAppsServer#
* @name _server
* @type {Server}
*/
//@ts-ignore
this._server = options.server ? options.server : require("http").createServer();
/**
* @memberof SGAppsServer#
* @name _decorators
* @type {SGAppsServerDecorator[]}
*/;
let _decoratorsComputed = [ ..._decorators, ...(options.decorators || []) ];
Object.defineProperty(
this,
'_decorators',
{
get: () => _decoratorsComputed,
set: (decorators) => {
_decoratorsComputed = [ ..._decorators, ...decorators ];
},
enumerable: true,
configurable: false
}
);
/**
* @memberof SGAppsServer#
* @name _options
* @type {SGAppsServerOptions}
*/
this._options = Object.assign(
{ strictRouting: true }, (options || {})
);
/**
* @memberof SGAppsServer#
* @name STATUS_CODES
* @type {Object<number,string>}
*/
this.STATUS_CODES = require('http').STATUS_CODES;
/**
* @memberof SGAppsServer#
* @name shared
* @type {SGAppsServerShared}
*/
this.shared = {};
/**
* @memberof SGAppsServer#
* @name logger
* @type {LoggerBuilder}
*/
this.logger = new LoggerBuilder();
this.logger._debug = options.debug;
/**
* @memberof SGAppsServer#
* @method Email
* @param {SGAppsServerEmail.Config} config
* @returns {SGAppsServerEmail}
*/
//@ts-ignore
this.Email = _EmailBuilder;
/**
* @memberof SGAppsServer#
* @name mountPath
* @type {string}
*/
let _mountPath = '';
Object.defineProperty(
this,
'mountPath',
{
get: () => _mountPath,
set: (path) => {
_mountPath = path;
},
enumerable: true,
configurable: true
}
);
/**
* @memberof SGAppsServer#
* @name _fs
* @type {object}
*/;
this._fs = require('fs');
/**
* @memberof SGAppsServer#
* @name _path
* @type {object}
*/;
this._path = require('path');
/**
* @memberof SGAppsServer#
* @name EXTENSIONS
* @type {ResourcesExtensions}
*/
this.EXTENSIONS = _Extensions;
/**
* @memberof SGAppsServer#
* @name _requestListeners
* @type {Object<string,SGAppsServerDictionary>}
*/
this._requestListeners = {
"use" : new SGAppsServerDictionary({ name: "use" }),
"post" : new SGAppsServerDictionary({ name: "post" }),
"get" : new SGAppsServerDictionary({ name: "get" }),
"head" : new SGAppsServerDictionary({ name: "head" }),
"put" : new SGAppsServerDictionary({ name: "put" }),
"trace" : new SGAppsServerDictionary({ name: "trace" }),
"delete" : new SGAppsServerDictionary({ name: "delete" }),
"options" : new SGAppsServerDictionary({ name: "options" }),
"connect" : new SGAppsServerDictionary({ name: "connect" }),
"patch" : new SGAppsServerDictionary({ name: "patch" }),
"_finalHandler" : new SGAppsServerDictionary({
name: "_finalHandler",
reverse: true
})
};
this._server.on(
'request',
(
/**
* @param {IncomingMessage} request
* @param {ServerResponse} response
* @this {SGAppsServer}
*/
function (request, response) {
this.handle(
request,
response
);
}
).bind(this)
);
this._server.on(
'upgrade',
/**
*
* @param {IncomingMessage} request
* @param {Duplex} socket
* @param {Buffer} data
*/
function (request, socket, data) {
}
);
/**
* default value is `16 Kb` ยป `16 * 1024`
* @memberof SGAppsServer#
* @name MAX_POST_SIZE
* @type {number}
*/
var MAX_POST_SIZE = 16 * 1024;
Object.defineProperty(this, 'MAX_POST_SIZE', {
get: function () {
return MAX_POST_SIZE;
},
set: (value) => {
if (typeof(value) === "number") {
MAX_POST_SIZE = value;
} else {
this.logger.error('MAX_POST_SIZE should be a number');
}
}
});
/**
* @memberof SGAppsServer#
* @name whenReady
* @type {Promise<SGAppsServer>}
*/
this.whenReady = new Promise((resolve, reject) => {
let index = 0;
// @ts-ignore
const _decorators = this._decorators;
const loadDecorator = () => {
if (_decorators[index]) {
if (this.logger._debug) {
process.stderr.write(
`\n\tยป \x1b[36m[Server.Decorator] ${
_decorators[index].name || _decorators[index].toString()
}\x1b[0m`
);
}
_decorators[index](
null,
null,
this,
(err) => {
if (err) {
process.stderr.write(
' \x1b[32;1mError\x1b[0m\n' + err.message + '\n'
);
reject(err);
} else {
index++;
if (this.logger._debug) {
process.stderr.write(' \x1b[32;1mDone\x1b[0m');
}
loadDecorator();
}
}
);
} else {
if (this.logger._debug) {
process.stderr.write('\n\n\t\t\x1b[32;1mDecorators Loaded\x1b[0m\n');
}
resolve(this);
}
};
loadDecorator();
});
return this;
}
/**
* @memberof SGAppsServer
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {SGAppsServerDictionaryRunCallBack} callback
*/
SGAppsServer.prototype.handleRequest = function (request, response, callback) {
const method = (request.request.method || 'get').toLocaleLowerCase();
if (
method && method[0] !== "_" && (
method in this._requestListeners
)
) {
this._requestListeners.use.run(
request,
response,
//@ts-ignore
this,
(request, response, server) => {
this._requestListeners[method].run(
request,
response,
//@ts-ignore
this,
(request, response, server) => {
server._requestListeners._finalHandler.run(
request,
response,
server,
() => {
if (callback) {
callback(request, response, server);
} else if (
!response._flags.finished
) {
response.sendError(
Error(`Unable to handle path ${request.request ? request.request.url : ''}`),
{
statusCode: 404
}
);
}
}
);
}
);
}
);
} else {
response.sendError(Error(`[Request.method] is unknown; ${method}`));
}
};
/**
* @memberof SGAppsServer
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {Error} [err]
*/
SGAppsServer.prototype.handleErrorRequest = function (request, response, err) {
response.sendError(err || Error('unknown error'), {
statusCode: 500
});
};
/**
* @memberof SGAppsServer
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {string} path
* @param {SGAppsServerErrorCallBack} callback
* @param {object} [options]
* @param {number} [options.timeout=0]
* @param {string[]} [options.autoIndex] list of auto-index files, ex: ['index.html', 'index.htm', 'default.html']
*/
SGAppsServer.prototype.handleStaticRequest = function (request, response, path, callback, options = { timeout: 0, autoIndex: [] }) {
if (response.response) {
response.pipeFileStatic(
this._path.join(
this._path.resolve(path),
this._path.resolve(
request.mountPath || './',
this._path.resolve(
'/',
request.urlInfo.pathname
)
)
),
(request.urlInfo.pathname || '').replace(
/^.*\//,
''
) || 'index.html',
(err) => {
if (callback) {
callback(
err || null,
request,
response,
//@ts-ignore
this
);
} else {
if (err) {
response.sendError(
Error(`404 ${this.STATUS_CODES[404]}; ${err.message}`),
{
statusCode: 404
}
);
}
}
},
options
);
} else {
if (callback) {
callback(
Error('[Response] is already finished'),
request,
response,
//@ts-ignore
this
);
}
}
};
/**
* @memberof SGAppsServer
* @param {IncomingMessage} request
* @param {ServerResponse} response
* @param {SGAppsServerDictionaryRunCallBack} [callback]
*/
SGAppsServer.prototype.handle = function (request, response, callback) {
/**
* @private
* @type {SGAppsServerRequest}
*/
//@ts-ignore
const _request = new SGAppsServerRequest(request, this);
/**
* @private
* @type {SGAppsServerResponse}
*/
//@ts-ignore
const _response = new SGAppsServerResponse(response, this);
let index = 0;
const loadDecorator = () => {
//@ts-ignore
const _decorators = this._decorators;
if (_decorators[index]) {
_decorators[index](
_request,
_response,
// @ts-ignore
this,
(err) => {
if (err) {
_response.sendError(
Error(
`Unable to load [Request.Decorator]: ${_decorators[index].name} ; ${err.message}`
)
);
} else {
index++;
loadDecorator();
}
}
);
} else {
this.handleRequest(
_request,
_response,
function () {
if (callback) {
callback(_request, _response, this);
} else {
_response.sendError(
Error(`Unable to handle path ${request.url}`),
{
statusCode: 404
}
);
}
}
);
}
};
loadDecorator();
// // Improve garbage collector
// _response._destroy.push(function () {
// delete _request.request;
// delete _response.response;
// });
};
/**
* @memberof SGAppsServer
* @returns {Server}
*/
SGAppsServer.prototype.server = function () {
return this._server;
};
/**
* @memberof SGAppsServer
* @param {string|RequestHandler} path
* @param {...RequestHandler} [handlers]
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.use = function (path, ...handlers) {
this._requestListeners.use.push(
(typeof(path) === "string" || (path instanceof RegExp)) ? path : '',
//@ts-ignore
typeof(path) === "function" ? [path, ...handlers] : handlers
);
return this;
};
/**
* The `POST` method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.post = function (path, ...handlers) {
this._requestListeners.post.push(
path,
handlers
);
return this;
};
/**
* The `GET` method requests a representation of the specified resource. Requests using GET should only retrieve data.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.get = function (path, ...handlers) {
this._requestListeners.get.push(
path,
handlers
);
return this;
};
/**
* The `HEAD` method asks for a response identical to that of a GET request, but without the response body.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.head = function (path, ...handlers) {
this._requestListeners.head.push(
path,
handlers
);
return this;
};
/**
* The `PUT` method replaces all current representations of the target resource with the request payload.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.put = function (path, ...handlers) {
this._requestListeners.put.push(
path,
handlers
);
return this;
};
/**
* The `TRACE` method performs a message loop-back test along the path to the target resource.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.trace = function (path, ...handlers) {
this._requestListeners.trace.push(
path,
handlers
);
return this;
};
/**
* The `DELETE` method deletes the specified resource.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.delete = function (path, ...handlers) {
this._requestListeners.delete.push(
path,
handlers
);
return this;
};
/**
* The `OPTIONS` method is used to describe the communication options for the target resource.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.options = function (path, ...handlers) {
this._requestListeners.options.push(
path,
handlers
);
return this;
};
/**
* The `CONNECT` method establishes a tunnel to the server identified by the target resource.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.connect = function (path, ...handlers) {
this._requestListeners.connect.push(
path,
handlers
);
return this;
};
/**
* The `PATCH` method is used to apply partial modifications to a resource.
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.patch = function (path, ...handlers) {
this._requestListeners.patch.push(
path,
handlers
);
return this;
};
/**
* add handler to all methods
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.all = function (path, ...handlers) {
Object.keys(
this._requestListeners
).forEach((method) => {
if (method !== "use" && method[0] !== '_' && method[0] !== 'finalHandler') {
this._requestListeners[method].push(
path,
handlers
);
}
});
return this;
};
/**
* add final handler to all methods, last added is first
*
* @memberof SGAppsServer
* @param {RequestPathStructure} path
* @param {...RequestHandler} handlers
* @returns {SGAppsServer}
*/
SGAppsServer.prototype.finalHandler = function (path, ...handlers) {
this._requestListeners._finalHandler.push(
path,
handlers
);
return this;
};
/**
* @callback SGAppsServerHandlerPostData
* @param {SGAppsServerRequest} request
* @param {SGAppsServerResponse} response
* @param {function} next
*/
/**
* @memberof SGAppsServer
* @param {object} [options]
* @param {number} [options.MAX_POST_SIZE]
* @param {object} [options.error]
* @param {number} options.error.statusCode
* @param {string} [options.error.message]
* @returns {SGAppsServerHandlerPostData}
*/
SGAppsServer.prototype.handlePostData = function (options) {
return (request, response, next) => {
options = Object.assign(
{
error: {
statusCode: 500
}
}, options
);
if ("MAX_POST_SIZE" in options) {
request.MAX_POST_SIZE = options.MAX_POST_SIZE;
}
request.postData.then(
() => {
next();
},
(err) => {
response.sendError(
Error(
options.error.message
|| this.STATUS_CODES[options.error.statusCode]
|| err.message
|| 'Unexpected Error'
),
{
statusCode: options.error.statusCode
}
);
}
);
};
};
module.exports = SGAppsServer;