lib/application.js

Summary

Maintainability
A
0 mins
Test Coverage
const url = require('url');
const art = require('art-template');
const path = require('path');
const cookie = require('cookie');
const helper = require('./helper');
const Container = require('varal-container');
const multiparty = require('multiparty');
const serveStatic = require('serve-static');
const queryString = require('querystring');
const cookieSignature = require('cookie-signature');

class Application extends Container {

    constructor(varal, req, res) {
        super();
        this.binds = Object.assign({}, varal.binds);
        this.instances = Object.assign({}, varal.instances);
        this.config = varal.config;
        this.router = varal.router;
        this.middleware = varal.middleware;
        this.emitter = varal.emitter;
        this.log = varal.log;
        this.bind('varal.app', this);
        this.initReq(req);
        this.initRes(res);
    }

    initReq(req) {
        this.req = req;
        this.path = helper.pathFormat(url.parse(req.url).pathname);
        this.files = [];
        this.fields = url.parse(req.url, true).query;
        this.cookies = cookie.parse(req.headers.cookie || '');
    }

    initRes(res) {
        this.res = res;
        this.next = true;
        this.asyncStuck = 0;
        this.resEndWith = 'null';
        this.resStatus = 200;
        this.resStatusMessage = '';
        this.resHeaders = {};
        this.resCookies = [];
        this.resBody = [];
        this.routeResult = undefined;
        this.data = {};
    }

    error(err, options) {
        const {
            log = true,
            show = true,
            exit = false,
            status = 500,
            title = 'Error',
            msg = 'Something went Wrong!'
        } = options || {};
        if (log)
            this.emit('error', err, exit);
        this.initRes(this.res);
        this.setStatus(status);
        this.html(art(path.join(__dirname, 'views/error.html'), {
            title,
            msg,
            debug: this.config.debug && show,
            err: err.stack || err,
        }));
        this.resEnd();
    }

    renderError(status, msg) {
        const handler = this.make('app.e' + status);
        if (typeof handler === 'function')
            handler(this);
        else
            this.error(new Error(msg), {
                log: false,
                show: false,
                status: 404,
                title: msg,
                msg
            });
    }

    emit(eventName, ...args) {
        return this.emitter.emit(eventName, ...args);
    }

    setStatus(status) {
        this.resStatus = status;
    }

    setStatusMessage(message) {
        this.resStatusMessage = message;
    }

    setHeaders(headers) {
        Object.assign(this.resHeaders, headers);
    }

    setHeader(name, value) {
        Object.assign(this.resHeaders, {[name]: value});
    }

    removeHeader(name) {
        delete this.resHeaders[name];
    }

    clearHeaders() {
        this.resHeaders = {};
    }

    sentHeaders() {
        if (this.resCookies.length > 0)
            this.setHeader('Set-Cookie', this.resCookies);
        for (let name in this.resHeaders)
            this.res.setHeader(name, this.resHeaders[name]);
    }

    write(value) {
        if (typeof value !== 'string' && typeof value !== Buffer)
            return;
        this.resBody.push(value);
    }

    sentBody() {
        for (let i = 0; i < this.resBody.length; i += 1)
            this.res.write(this.resBody[i]);
    }

    resSent() {
        if (this.resIsEnd())
            return;
        this.res.statusCode = this.resStatus;
        if (this.resStatusMessage !== '')
            this.res.statusMessage = this.resStatusMessage;
        this.sentHeaders();
        this.sentBody();
    }

    async handle() {
        await this.applyForm();
        this.applyGlobalMiddleware();
        this.applyRoutes();
        this.applyStatic();
        this.resEnd();
    }

    async applyForm() {
        const contentType = this.req.headers['content-type'] || '';
        if (this.req.method === 'POST') {
            if (contentType.indexOf('application/x-www-form-urlencoded') >= 0) {
                await this.parseForm();
            } else if (contentType.indexOf('multipart/form-data') >= 0) {
                await this.parseMultipartForm();
            }
        }
    }

    parseForm() {
        const self = this;
        return new Promise((resolve, reject) => {
            this.req.setEncoding('utf8');
            this.req.on('data', chunk => {
                chunk = queryString.parse(chunk);
                Object.assign(self.fields, chunk);
            });
            this.req.on('end', () => {
                resolve();
            });
            this.req.on('error', err => {
                reject(err);
            });
        });
    }

    parseMultipartForm() {
        const self = this;
        const form = new multiparty.Form();
        return new Promise((resolve, reject) => {
            form.parse(this.req, (err, fields, files) => {
                if (err)
                    reject(err);
                self.files = files ? files.upload : [];
                for (let value in fields)
                    if (fields[value].length === 1)
                        fields[value] = fields[value][0];
                Object.assign(self.fields, fields);
                resolve();
            });
        });
    }

    applyGlobalMiddleware() {
        this.middleware.handleGlobal(this);
    }

    applyRoutes() {
        if (this.resIsEnd())
            return;
        const result = this.router.getCallback(this.req.method, this.path);
        if (result.path_match === true) {
            this.resEndWith = 'route';
            if (result.method_match === true) {
                this.routeResult = result;
                this.middleware.handle(result.middleware, this);
            } else
                this.renderError(405, '405 Method Not Allowed');
        }
    }

    applyRouteResult() {
        if (this.resEndWith !== 'route' || this.next === false || this.asyncStuck !== 0)
            return;
        const result = this.routeResult;
        if (typeof result.callback === 'function')
            this.asyncHandler(result.callback(this, ...result.args));
        else if (typeof result.callback === 'string') {
            const action = result.callback.split('@');
            const controllerPath = path.join(this.config.rootPath, this.config.controllerPath, action[0]);
            const controller = this.make(require(controllerPath));
            this.asyncHandler(controller[action[1]](this, ...result.args));
        }
    }

    applyStatic() {
        if (this.resIsEnd())
            return;
        if (this.resEndWith === 'route')
            return;
        this.resEndWith = 'static';
        const self = this;
        serveStatic(this.config.staticPath)(this.req, this.res, () => {
            self.resEndWith = 'null';
            self.renderError(404, '404 Not Found');
        });
    }

    asyncHandler(callback) {
        if (callback instanceof Promise) {
            this.asyncStuck += 1;
            callback.then(() => {
                this.asyncStuck -= 1;
                this.resEnd();
            }).catch(err => {
                this.asyncStuck -= 1;
                this.error(err);
            });
        }
    }

    resEnd(msg) {
        if (this.resIsEnd() || this.resEndWith === 'static' || this.asyncStuck !== 0)
            return;
        this.resSent();
        this.res.end(msg);
    }

    resIsEnd() {
        return this.res.finished;
    }

    text(text) {
        if (typeof text !== 'string' && typeof text.toString === 'function')
            text = text.toString();
        this.setHeader('Content-Type', 'text/plain');
        this.write(text);
    }

    html(data) {
        this.setHeader('Content-Type', 'text/html');
        this.write(data);
    }

    json(data) {
        data = JSON.stringify(data);
        this.setHeader('Content-Type', 'application/json');
        this.write(data);
    }

    render(view, data) {
        data = data || {};
        data.$app = this;
        view = path.join(this.config.rootPath, this.config.viewPath, view + '.html');
        this.html(art(view, data));
    }

    route(name, ...args) {
        let callback = this.router.getCallbackByName(name);
        if (callback) {
            this.routeResult = {callback, args};
            this.applyRouteResult();
        }
    }

    redirect(url) {
        this.setStatus(302);
        this.setHeader('Location', url);
        this.text('Redirecting to ' + url);
    }

    setCookie(name, value, options) {
        options = options || {};
        if (typeof options.key === 'string' && options.key !== '')
            value = cookieSignature.sign(value, options.key);
        this.resCookies.push(cookie.serialize(name, value, options));
    }

    getCookie(name, key) {
        let data = this.cookies[name];
        if (typeof data !== 'string')
            return false;
        if (typeof key === 'string' && key !== '')
            data = cookieSignature.unsign(data, key);
        return data;
    }

    service(name) {
        const [file, attr] = name.split('@');
        const service = path.join(this.config.rootPath, this.config.servicePath, file);
        const instance = this.make(require(service));
        if (attr !== undefined)
            return instance[attr];
        return instance;
    }

}

exports = module.exports = Application;