@wellenline/via
Version: 
Lightweight express like web framework
396 lines (395 loc) • 15.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HttpException = exports.CustomErrorHandler = exports.Context = exports.Options = exports.Head = exports.Mixed = exports.Delete = exports.Patch = exports.Put = exports.Post = exports.Get = exports.Params = exports.Route = exports.Before = exports.Resource = exports.bootstrap = exports.app = exports.Constants = exports.HttpMethodsEnum = exports.HttpStatus = void 0;
const http_1 = require("http");
var HttpStatus;
(function (HttpStatus) {
    HttpStatus[HttpStatus["CONTINUE"] = 100] = "CONTINUE";
    HttpStatus[HttpStatus["SWITCHING_PROTOCOLS"] = 101] = "SWITCHING_PROTOCOLS";
    HttpStatus[HttpStatus["PROCESSING"] = 102] = "PROCESSING";
    HttpStatus[HttpStatus["OK"] = 200] = "OK";
    HttpStatus[HttpStatus["CREATED"] = 201] = "CREATED";
    HttpStatus[HttpStatus["ACCEPTED"] = 202] = "ACCEPTED";
    HttpStatus[HttpStatus["NON_AUTHORITATIVE_INFORMATION"] = 203] = "NON_AUTHORITATIVE_INFORMATION";
    HttpStatus[HttpStatus["NO_CONTENT"] = 204] = "NO_CONTENT";
    HttpStatus[HttpStatus["RESET_CONTENT"] = 205] = "RESET_CONTENT";
    HttpStatus[HttpStatus["PARTIAL_CONTENT"] = 206] = "PARTIAL_CONTENT";
    HttpStatus[HttpStatus["AMBIGUOUS"] = 300] = "AMBIGUOUS";
    HttpStatus[HttpStatus["MOVED_PERMANENTLY"] = 301] = "MOVED_PERMANENTLY";
    HttpStatus[HttpStatus["FOUND"] = 302] = "FOUND";
    HttpStatus[HttpStatus["SEE_OTHER"] = 303] = "SEE_OTHER";
    HttpStatus[HttpStatus["NOT_MODIFIED"] = 304] = "NOT_MODIFIED";
    HttpStatus[HttpStatus["TEMPORARY_REDIRECT"] = 307] = "TEMPORARY_REDIRECT";
    HttpStatus[HttpStatus["PERMANENT_REDIRECT"] = 308] = "PERMANENT_REDIRECT";
    HttpStatus[HttpStatus["BAD_REQUEST"] = 400] = "BAD_REQUEST";
    HttpStatus[HttpStatus["UNAUTHORIZED"] = 401] = "UNAUTHORIZED";
    HttpStatus[HttpStatus["PAYMENT_REQUIRED"] = 402] = "PAYMENT_REQUIRED";
    HttpStatus[HttpStatus["FORBIDDEN"] = 403] = "FORBIDDEN";
    HttpStatus[HttpStatus["NOT_FOUND"] = 404] = "NOT_FOUND";
    HttpStatus[HttpStatus["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
    HttpStatus[HttpStatus["NOT_ACCEPTABLE"] = 406] = "NOT_ACCEPTABLE";
    HttpStatus[HttpStatus["PROXY_AUTHENTICATION_REQUIRED"] = 407] = "PROXY_AUTHENTICATION_REQUIRED";
    HttpStatus[HttpStatus["REQUEST_TIMEOUT"] = 408] = "REQUEST_TIMEOUT";
    HttpStatus[HttpStatus["CONFLICT"] = 409] = "CONFLICT";
    HttpStatus[HttpStatus["GONE"] = 410] = "GONE";
    HttpStatus[HttpStatus["LENGTH_REQUIRED"] = 411] = "LENGTH_REQUIRED";
    HttpStatus[HttpStatus["PRECONDITION_FAILED"] = 412] = "PRECONDITION_FAILED";
    HttpStatus[HttpStatus["PAYLOAD_TOO_LARGE"] = 413] = "PAYLOAD_TOO_LARGE";
    HttpStatus[HttpStatus["URI_TOO_LONG"] = 414] = "URI_TOO_LONG";
    HttpStatus[HttpStatus["UNSUPPORTED_MEDIA_TYPE"] = 415] = "UNSUPPORTED_MEDIA_TYPE";
    HttpStatus[HttpStatus["REQUESTED_RANGE_NOT_SATISFIABLE"] = 416] = "REQUESTED_RANGE_NOT_SATISFIABLE";
    HttpStatus[HttpStatus["EXPECTATION_FAILED"] = 417] = "EXPECTATION_FAILED";
    HttpStatus[HttpStatus["I_AM_A_TEAPOT"] = 418] = "I_AM_A_TEAPOT";
    HttpStatus[HttpStatus["UNPROCESSABLE_ENTITY"] = 422] = "UNPROCESSABLE_ENTITY";
    HttpStatus[HttpStatus["TOO_MANY_REQUESTS"] = 429] = "TOO_MANY_REQUESTS";
    HttpStatus[HttpStatus["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
    HttpStatus[HttpStatus["NOT_IMPLEMENTED"] = 501] = "NOT_IMPLEMENTED";
    HttpStatus[HttpStatus["BAD_GATEWAY"] = 502] = "BAD_GATEWAY";
    HttpStatus[HttpStatus["SERVICE_UNAVAILABLE"] = 503] = "SERVICE_UNAVAILABLE";
    HttpStatus[HttpStatus["GATEWAY_TIMEOUT"] = 504] = "GATEWAY_TIMEOUT";
    HttpStatus[HttpStatus["HTTP_VERSION_NOT_SUPPORTED"] = 505] = "HTTP_VERSION_NOT_SUPPORTED";
})(HttpStatus = exports.HttpStatus || (exports.HttpStatus = {}));
var HttpMethodsEnum;
(function (HttpMethodsEnum) {
    HttpMethodsEnum["GET"] = "GET";
    HttpMethodsEnum["POST"] = "POST";
    HttpMethodsEnum["PUT"] = "PUT";
    HttpMethodsEnum["DELETE"] = "DELETE";
    HttpMethodsEnum["PATCH"] = "PATCH";
    HttpMethodsEnum["MIXED"] = "MIXED";
    HttpMethodsEnum["HEAD"] = "HEAD";
    HttpMethodsEnum["OPTIONS"] = "OPTIONS";
})(HttpMethodsEnum = exports.HttpMethodsEnum || (exports.HttpMethodsEnum = {}));
var Constants;
(function (Constants) {
    Constants["INVALID_ROUTE"] = "Invalid route";
    Constants["NO_RESPONSE"] = "No response";
    Constants["ROUTE_DATA"] = "__route_data__";
    Constants["ROUTE_MIDDLEWARE"] = "__route_middleware__";
    Constants["ROUTE_PARAMS"] = "__route_params__";
})(Constants = exports.Constants || (exports.Constants = {}));
exports.app = {
    headers: {
        "Content-type": "application/json",
    },
    base: "http://via.local",
    middleware: [],
    next: false,
    logs: false,
    routes: [],
    resources: [],
    instances: [],
};
const decorators = {
    route: [],
    param: [],
    middleware: [],
};
const bootstrap = (options) => {
    if (options.middleware) {
        exports.app.middleware = options.middleware;
    }
    if (options.resources) {
        exports.app.resources = options.resources;
    }
    if (options.base) {
        exports.app.base = options.base;
    }
    if (options.logs) {
        exports.app.logs = options.logs;
    }
    exports.app.server = (0, http_1.createServer)(onRequest).listen(options.port);
};
exports.bootstrap = bootstrap;
/**
 * Convert path to regex
 * @param str string
 * @returns new RegExp
 */
const regexify = (str) => {
    const parts = str.split("/");
    parts.shift();
    const pattern = parts.map((part) => {
        if (part.startsWith("*")) {
            return `/(.*)`;
        }
        else if (part.startsWith(":")) {
            if (part.endsWith("?")) {
                return `(?:/([^/]+?))?`;
            }
            return `/([^/]+?)`;
        }
        return `/${part}`;
    });
    return {
        pattern: new RegExp(`^${pattern.join("")}/?$`, "i"),
    };
};
/**
 * Resource decorator
 * @param path route path
 */
const Resource = (path = "", options) => {
    return (target) => {
        const resource_before = [];
        const resource = decorators.middleware.find((m) => m.resource && m.target === target);
        if (resource && resource.middleware) {
            resource_before.push(...resource.middleware); // = middleware.concat(resource.middleware);
        }
        const routes = decorators.route.filter((route) => route.target === target);
        exports.app.routes = exports.app.routes.concat(routes.map((route) => {
            const func = decorators.middleware.find((m) => m.descriptor && m.descriptor.value === route.descriptor.value && m.target === route.target);
            const params = decorators.param.filter((m) => route.name === m.name && m.target === route.target);
            const route_before = [];
            if (func && func.middleware) {
                route_before.push(...func.middleware); // = middleware.concat(func.middleware);
            }
            if (!exports.app.instances[route.target]) {
                exports.app.instances[route.target] = new route.target();
            }
            return {
                target: route.target,
                fn: route.descriptor.value.bind(exports.app.instances[route.target]),
                path: options && options.version ? "/" + options.version + path + route.path : path + route.path,
                middleware: resource_before.concat(route_before),
                params,
                name: route.name,
                method: route.method,
            };
        }));
    };
};
exports.Resource = Resource;
// Decorators
const Before = (...middleware) => {
    return (target, name, descriptor) => {
        decorators.middleware.push({ middleware, resource: descriptor ? false : true, descriptor, target: descriptor ? target.constructor : target });
    };
};
exports.Before = Before;
/**
 * @Route Decorator
 * @param method HttpMethodsEnum
 * @param path Route path
 */
const Route = (method, path, middleware) => (target, name, descriptor) => {
    decorators.route.push({ method, path, name, middleware, descriptor, target: target.constructor });
};
exports.Route = Route;
const Params = (fn) => (target, name, index) => decorators.param.push({ index, name, fn, target: target.constructor });
exports.Params = Params;
const Get = (path) => (0, exports.Route)(HttpMethodsEnum.GET, path);
exports.Get = Get;
const Post = (path) => (0, exports.Route)(HttpMethodsEnum.POST, path);
exports.Post = Post;
const Put = (path) => (0, exports.Route)(HttpMethodsEnum.PUT, path);
exports.Put = Put;
const Patch = (path) => (0, exports.Route)(HttpMethodsEnum.PATCH, path);
exports.Patch = Patch;
const Delete = (path) => (0, exports.Route)(HttpMethodsEnum.DELETE, path);
exports.Delete = Delete;
const Mixed = (path) => (0, exports.Route)(HttpMethodsEnum.MIXED, path);
exports.Mixed = Mixed;
const Head = (path) => (0, exports.Route)(HttpMethodsEnum.HEAD, path);
exports.Head = Head;
const Options = (path) => (0, exports.Route)(HttpMethodsEnum.OPTIONS, path);
exports.Options = Options;
const Context = (key) => (0, exports.Params)((req) => !key ? req.context : req.context[key]);
exports.Context = Context;
/**
 * Request handler
 * @param req Request
 * @param res Response
 */
const onRequest = async (req, res) => {
    try {
        req.params = {};
        req.parsed = new URL(req.url, exports.app.base);
        exports.app.next = true;
        req.route = getRoute(req);
        req.params = decodeValues(req.params);
        req.query = decodeValues(Object.fromEntries(req.parsed.searchParams));
        req.context = {
            status: HttpStatus.OK,
            headers: exports.app.headers,
            params: req.params,
            route: req.route,
            query: req.query,
            req,
            res,
        };
        if (exports.app.middleware.length > 0) {
            await execute(exports.app.middleware, req.context);
        }
        if (!req.route) {
            throw new HttpException(Constants.INVALID_ROUTE, HttpStatus.NOT_FOUND);
        }
        if (req.route.middleware && exports.app.next) {
            await execute(req.route.middleware, req.context);
        }
        if (!exports.app.next) {
            return;
        }
        req.context.res.body = await req.route.fn(...args(req));
        resolve(req.context);
    }
    catch (e) {
        if (exports.app.logs) {
            console.error(e);
        }
        res.writeHead(e.status || HttpStatus.INTERNAL_SERVER_ERROR, { ...exports.app.headers, ...e.headers });
        res.write(JSON.stringify({
            message: e.message,
            statusCode: e.status,
        }));
        res.end();
    }
};
/**
 * Run middleware
 * @param list
 * @param context
 */
const execute = async (list, context) => {
    for (const fn of list) {
        exports.app.next = false;
        if (fn instanceof Function) {
            const result = await fn(context);
            if (!result) {
                break;
            }
            exports.app.next = true;
        }
    }
};
/**
 * Find route and match params
 * @param req Request
 */
const getRoute = (req) => {
    return exports.app.routes.find((route) => {
        const { pathname } = new URL(req.url, exports.app.base);
        const keys = [];
        const regex = /:([^\/?]+)\??/g;
        route.path = route.path.endsWith("/") && route.path.length > 1 ? route.path.slice(0, -1) : route.path;
        let params = regex.exec(route.path);
        while (params !== null) {
            keys.push(params[1]);
            params = regex.exec(route.path);
        }
        const path = route.path.replace(/\/{1,}/g, "/");
        const { pattern } = regexify(path);
        const matches = pathname.match(pattern);
        if (matches && (route.method === req.method
            || route.method === HttpMethodsEnum.MIXED)) {
            req.params = Object.assign(req.params, keys.reduce((val, key, index) => {
                val[key] = matches[index + 1];
                return val;
            }, {}));
            return true;
        }
    });
};
const args = (req) => {
    const ARGS = [];
    if (req.route.params) {
        req.route.params.sort((a, b) => a.index - b.index);
        for (const param of req.route.params) {
            ARGS.push(param ? param.fn(req) : undefined);
        }
    }
    return ARGS;
};
/**
 * Decode values
 * @param obj parameter values
 */
const decodeValues = (obj) => {
    /*const decoded: { [x: string]: number | boolean | string } = {};
    for (const key of Object.keys(obj)) {
        decoded[key] = !isNaN(parseFloat(obj[key])) && isFinite(obj[key]) && !(Number.isSafeInteger(obj[key])) ?
            (Number.isInteger(obj[key]) ? Number.parseInt(obj[key], 10) :
                Number.parseFloat(obj[key])) : obj[key];
    }
    return decoded;*/
    const decodedValues = {};
    for (const key of Object.keys(obj)) {
        const value = obj[key];
        // Check if the value is a number
        const isNumeric = !isNaN(parseFloat(value)) && isFinite(value);
        if (isNumeric) {
            // Parse the number based on its type
            if (!Number.isSafeInteger(Number(value))) {
                decodedValues[key] = value.toString();
            }
            else if (Number.isInteger(value)) {
                decodedValues[key] = Number.parseInt(value, 10);
            }
            else {
                decodedValues[key] = Number.parseFloat(value);
            }
        }
        else {
            // Keep the original value if it's not a number
            decodedValues[key] = value;
        }
    }
    return decodedValues;
};
/**
 * Check if obj is stream
 * @param obj any
 */
const isStream = (obj) => obj &&
    typeof obj === "object" &&
    typeof obj.pipe === "function";
/**
 * Check if obj is object
 * @param obj any
 */
const isObject = (obj) => obj &&
    typeof obj === "object" && !Buffer.isBuffer(obj);
/**
 * Check if obj is readable
 * @param obj any
 */
const isReadable = (obj) => isStream(obj) &&
    typeof obj._read === "function" &&
    typeof obj._readableState === "object" || obj.readable;
/**
 * Resolve request
 * @param context request context
 */
const resolve = (context) => {
    if (context.redirect) {
        context.res.writeHead(context.status, { Location: context.redirect });
        return context.res.end();
    }
    context.res.writeHead(context.status, Object.assign({}, exports.app.headers, context.headers));
    if (isReadable(context.res.body)) {
        return context.res.body.pipe(context.res);
    }
    if (isObject(context.res.body)) {
        return context.res.end(JSON.stringify(context.res.body));
    }
    context.res.end(context.res.body || "");
};
class CustomErrorHandler extends Error {
}
exports.CustomErrorHandler = CustomErrorHandler;
/**
 * HttpException error
 */
class HttpException extends CustomErrorHandler {
    constructor(message, status = HttpStatus.INTERNAL_SERVER_ERROR, headers) {
        super(message);
        this.status = status;
        this.name = this.constructor.name;
        Error.captureStackTrace(this, this.constructor);
        this.status = status;
        if (headers) {
            this.headers = headers;
        }
    }
}
exports.HttpException = HttpException;