tspace-spear
Version:
tspace-spear is a lightweight, high-performance API framework for Node.js that leverages the native HTTP server and supports uWebSockets.js (C++) for maximum speed and efficiency.
1,111 lines • 41.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Spear = exports.Application = void 0;
const http_1 = __importStar(require("http"));
const const_1 = require("../const");
const stream_1 = require("stream");
const cluster_1 = __importDefault(require("cluster"));
const os_1 = __importDefault(require("os"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const on_finished_1 = __importDefault(require("on-finished"));
const ws_1 = __importDefault(require("ws"));
const net_1 = __importDefault(require("net"));
const parser_factory_1 = require("./parser-factory");
const fast_router_1 = require("./fast-router");
const response_1 = require("./response");
const compiler_1 = require("../compiler");
const uWS_1 = require("./uWS");
const net_2 = require("./net");
/**
*
* The 'Spear' class is used to create a server and handle HTTP requests.
*
* @returns {Spear} application
* @example
* new Spear()
* .get('/' , () => 'Hello world!')
* .get('/json' , () => {
* return {
* message : 'Hello world!'
* }
* })
* .listen(3000 , () => console.log('server listening on port : 3000'))
*
*/
class Spear {
_controllers;
_middlewares;
_router = new fast_router_1.FastRouter();
_parser = new parser_factory_1.ParserFactory();
_globalPrefix = '';
_adapter = { kind: 'http', server: http_1.default };
_cluster;
_cors;
_swagger = {
use: false,
path: '/api/docs',
servers: [
{
url: '/'
}
],
tags: [],
info: {
title: "API Documentation",
description: "This is a sample documentation",
version: "1.0.0"
}
};
_swaggerSpecs = [];
_ws = {
handler: null,
server: null,
options: null
};
_errorHandler = null;
_globalMiddlewares = [];
_formatResponse = null;
_onListeners = [];
_fileUploadOptions = {
limit: Infinity,
tempFileDir: 'tmp',
removeTempFile: {
remove: false,
ms: 1000 * 60 * 10
}
};
_generatePreRouteTypes;
constructor({ controllers, middlewares, globalPrefix, logger, cluster, adapter } = {}) {
this._controllers = controllers;
this._middlewares = middlewares;
if (logger)
this.useLogger();
if (cluster)
this.useCluster(cluster);
if (adapter)
this.useAdater(adapter);
if (globalPrefix)
this.useGlobalPrefix(globalPrefix);
// Ensure controllers is NOT an array and has the required shape
// before enabling automatic route generation (used for E2E typing).
const isValidControllerObject = controllers &&
!Array.isArray(controllers) &&
typeof controllers === "object" &&
"folder" in controllers &&
"name" in controllers &&
"preRouteTypes" in controllers &&
controllers.folder &&
controllers.name &&
controllers.preRouteTypes;
if (isValidControllerObject) {
// Auto-generate route metadata for type-safe E2E usage;
this._generatePreRouteTypes = {
folder: controllers.folder,
name: controllers.name,
};
}
}
/**
* The get 'instance' method is used to get the instance of Spear.
*
* @returns {this}
*/
get instance() {
return this;
}
/**
* The get 'routers' method is used get the all routers.
*
* @returns {FastRouter}
*/
get routers() {
return this._router;
}
get contract() {
return {};
}
/**
* The 'usePreRouteTypes' method is used to create pre routes for e2e and swagger
*
* @param {{object}} options options
* @property {string} options.folder
* @property {RegExp} options.name
* @returns {this}
*/
usePreRouteTypes(options) {
this._generatePreRouteTypes = options;
return this;
}
/**
* The 'ws' method is used to creates the WebSocket server.
*
* @callback {Function} WebSocketServer
* @param {WebSocketServer} wss - WebSocketServer
* @returns {this}
*/
ws(handlers, options) {
this._ws.handler = handlers();
this._ws.options = options ?? {};
return this;
}
/**
* The 'use' method is used to add the middleware into the request pipeline.
*
* @callback {Function} middleware
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
use(middleware) {
this._globalMiddlewares.push(middleware);
return this;
}
/**
* The 'useGlobalPrefix' method is used to sets a global prefix for all routes in the router.
*
* If `globalPrefix` is `null` or `undefined`, it will default to an empty string,
* meaning no prefix will be applied.
*
* @param {string | null} globalPrefix - The base path prefix to apply to all routes.
* @returns {this} Returns the current instance for method chaining.
*/
useGlobalPrefix(globalPrefix) {
this._globalPrefix = globalPrefix == null ? '' : globalPrefix.replace(/^\/+|\/+$/g, '');
return this;
}
/**
* The 'useAdater' method is used to switch between different server implementations,
* such as the native Node.js HTTP server or uWebSockets.js (uWS).
*
* @param {T.AdapterServer} adapter - The adapter instance (e.g., HTTP or uWS).
* @returns {this} Returns the current instance for chaining
*/
useAdater(adapter) {
if (adapter === http_1.default) {
this._adapter = { kind: 'http', server: adapter };
}
else if (adapter === net_1.default) {
this._adapter = { kind: 'net', server: adapter };
}
else {
//@ts-ignore
this._adapter = { kind: 'uWS', server: adapter };
}
this._parser.useAdater(this._adapter);
return this;
}
/**
* The 'useCluster' method is used cluster run the server
*
* @param {boolean | number} cluster
* @returns {this}
*/
useCluster(cluster) {
if (cluster === false)
return this;
this._cluster = cluster ?? true;
return this;
}
/**
* The 'useLogger' method is used to add the middleware view logger response.
*
* @callback {Function} middleware
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
useLogger({ methods, exceptPath } = {}) {
this._globalMiddlewares.push(({ req, res }, next) => {
const diffTime = (hrtime) => {
const MS = 1000;
if (hrtime == null)
return 0;
const [start, end] = process.hrtime(hrtime);
const time = ((start * MS) + (end / 1e6));
return `${time > MS ? `${(time / MS).toFixed(2)} s` : `${time.toFixed(2)} ms`}`;
};
const statusCode = (res) => {
const statusCode = res.statusCode == null ? 500 : Number(res.statusCode);
return statusCode < 400
? `\x1b[32m${statusCode}\x1b[0m`
: `\x1b[31m${statusCode}\x1b[0m`;
};
if (exceptPath instanceof RegExp && exceptPath.test(req.url))
return next();
if (Array.isArray(exceptPath) && exceptPath.some(v => req.url === v))
return next();
if (methods != null &&
methods.length &&
!methods.some(v => v.toLowerCase() === req.method.toLowerCase())) {
return next();
}
const startTime = process.hrtime();
(0, on_finished_1.default)(res, () => {
console.log([
`[\x1b[1m\x1b[34mINFO\x1b[0m]`,
`\x1b[34m${new Date().toJSON()}\x1b[0m`,
`\x1b[33m${req.method}\x1b[0m`,
`${decodeURIComponent(req.url)}`,
`${statusCode(res)}`,
`${diffTime(startTime)}`,
].join(" "));
});
return next();
});
return this;
}
/**
* The 'useBodyParser' method is a middleware used to parse the request body of incoming HTTP requests.
* @param {object?}
* @property {array?} except the body parser with some methods
* @returns {this}
*/
useBodyParser({ except } = {}) {
this._globalMiddlewares.push((ctx, next) => {
const { req, res } = ctx;
if (Array.isArray(except) &&
except.some(v => v.toLowerCase() === (req.method).toLowerCase())) {
return next();
}
const contentType = req?.headers['content-type'] ?? null;
if (contentType == null)
return next();
const isFileUpload = contentType && contentType.startsWith('multipart/form-data');
if (isFileUpload)
return next();
if (req?.body != null)
return next();
Promise.resolve(this._parser.body(req, res))
.then(body => {
req.body = body;
return next();
})
.catch(err => {
return this._nextError(ctx)(err);
});
});
return this;
}
/**
* The 'useFileUpload' method is a middleware used to handler file uploads. It adds a file upload of incoming HTTP requests.
*
* @param {?Object}
* @property {?number} limits // bytes. default Infinity
* @property {?string} tempFileDir
* @property {?Object} removeTempFile
* @property {boolean} removeTempFile.remove
* @property {number} removeTempFile.ms
* @returns
*/
useFileUpload({ limit, tempFileDir, removeTempFile } = {}) {
if (limit != null) {
this._fileUploadOptions.limit = limit;
}
if (tempFileDir != null) {
this._fileUploadOptions.tempFileDir = tempFileDir;
}
if (removeTempFile != null) {
this._fileUploadOptions.removeTempFile = removeTempFile;
}
this._globalMiddlewares.push((ctx, next) => {
const { req, res } = ctx;
if (req.method === 'GET') {
return next();
}
const contentType = req?.headers['content-type'];
const isFileUpload = contentType && contentType.startsWith('multipart/form-data');
if (!isFileUpload)
return next();
if (req?.files != null)
return next();
Promise
.resolve(this._parser.files({ req, res, options: this._fileUploadOptions }))
.then(r => {
req.files = r.files;
req.body = r.body;
return next();
})
.catch(err => {
return this._nextError(ctx)(err);
});
});
return this;
}
/**
* The 'useCookiesParser' method is a middleware used to parses cookies attached to the client request object.
*
* @returns {this}
*/
useCookiesParser() {
this._globalMiddlewares.push(({ req }, next) => {
if (req?.cookies != null)
return next();
req.cookies = this._parser.cookies(req);
return next();
});
return this;
}
/**
* The 'useRouter' method is used to add the router in the request context.
*
* @parms {Function} router
* @property {Function} router - get() , post() , put() , patch() , delete()
* @returns {this}
*/
useRouter(router) {
const routes = router.routes;
for (const { path, method, handlers } of routes) {
this[method](this._normalizePath(this._globalPrefix, path), ...handlers);
}
return this;
}
/**
* The 'useSwagger' method is a middleware used to create swagger api.
*
* @param {?Object} doc
* @returns
*/
useSwagger(doc = {}) {
const { path, servers, tags, info, options } = doc;
this._swagger = {
use: true,
options: options,
path: path ?? this._swagger.path,
servers: servers ?? this._swagger.servers,
tags: tags ?? this._swagger.tags,
info: info ?? this._swagger.info
};
return this;
}
/**
* The 'listen' method is used to bind and start a server to a particular port and optionally a hostname.
*
* @param {number} port
* @param {function} callback
* @returns
*/
async listen(port, hostname, callback) {
if (arguments.length === 2 && typeof hostname === 'function') {
callback = hostname;
}
const server = await this._createServer();
if (this._generatePreRouteTypes) {
await new compiler_1.Compiler().generateRoutes(this._globalPrefix, this._generatePreRouteTypes);
}
if (this._cluster != null &&
this._cluster || typeof this._cluster === 'number') {
this._clusterMode({
server,
port,
hostname,
callback
});
return server;
}
if (this._adapter.kind === 'uWS') {
const handler = async () => {
this._onListeners.forEach(listener => listener());
if (this._swagger.use) {
await this._swaggerHandler();
}
callback?.({ server, port });
};
if (hostname) {
server.listen(port, String(hostname), handler);
}
else {
server.listen(port, handler);
}
return server;
}
const args = hostname
? [port, hostname, () => callback?.({ server, port: port })]
: [port, () => callback?.({ server, port: port })];
server.listen(...args);
server.on('listening', async () => {
this._onListeners.forEach(listener => listener());
if (this._swagger.use) {
await this._swaggerHandler();
}
});
return server;
}
/**
* The 'cors' is used to enable the cors origins on the server.
*
* @params {Object}
* @property {(string | RegExp)[]} origins
* @property {boolean} credentials
* @returns
*/
cors({ origins, credentials } = {}) {
this._cors = ((req, res) => {
const origin = req.headers?.origin ?? null;
if (origin == null)
return;
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (Array.isArray(origins) && origins.length) {
for (const o of origins) {
if (typeof o === 'string' && (o === origin || o === '*')) {
res.setHeader('Access-Control-Allow-Origin', origin);
continue;
}
if (o instanceof RegExp && o.test(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
}
}
if (credentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (req.method === 'OPTIONS') {
res.writeHead(204, { 'Content-Length': '0' });
res.end();
return;
}
return;
});
return this;
}
/**
* The 'response' method is used to format the response
*
* @param {function} format
* @returns
*/
response(format) {
this._formatResponse = format;
return this;
}
/**
* The 'catch' method is middleware that is specifically designed to handle errors.
*
* that occur during the processing of requests
*
* @param {function} error
* @returns
*/
catch(error) {
this._errorHandler = error;
return this;
}
/**
* The 'notfound' method is middleware that is specifically designed to handle errors notfound that occur during the processing of requests
*
* @param {function} fn
* @returns
*/
notfound(fn) {
const handler = ({ req, res }) => {
const ctx = this._createContext({ req, res, ps: {} });
return fn(ctx);
};
this.all('*', handler);
return this;
}
/**
* The 'get' method is used to add the request handler to the router for the 'GET' method.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
get(path, ...handlers) {
this._onListeners.push(() => {
return this._router.get(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'post' method is used to add the request handler to the router for the 'POST' method.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
post(path, ...handlers) {
this._onListeners.push(() => {
return this._router.post(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'put' method is used to add the request handler to the router for the 'PUT' method.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
put(path, ...handlers) {
this._onListeners.push(() => {
return this._router.put(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'patch' method is used to add the request handler to the router for the 'PATCH' method.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
patch(path, ...handlers) {
this._onListeners.push(() => {
return this._router.patch(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'delete' method is used to add the request handler to the router for the 'DELETE' method.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {Object} ctx - context { req , res , query , params , cookies , files , body}
* @property {Function} next - go to next function
* @returns {this}
*/
delete(path, ...handlers) {
this._onListeners.push(() => {
return this._router.delete(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'head' method is used to add the request handler to the router for 'HEAD' methods.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {object} ctx - context { req , res , query , params , cookies , files , body}
* @property {function} next - go to next function
* @returns {this}
*/
head(path, ...handlers) {
this._onListeners.push(() => {
return this._router.head(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'head' method is used to add the request handler to the router for 'HEAD' methods.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {object} ctx - context { req , res , query , params , cookies , files , body}
* @property {function} next - go to next function
* @returns {this}
*/
options(path, ...handlers) {
this._onListeners.push(() => {
return this._router.options(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
/**
* The 'all' method is used to add the request handler to the router for 'GET' 'POST' 'PUT' 'PATCH' 'DELETE' 'HEAD' 'OPTIONS' methods.
*
* @param {string} path
* @callback {...Function[]} handlers of the middlewares
* @property {object} ctx - context { req , res , query , params , cookies , files , body}
* @property {function} next - go to next function
* @returns {this}
*/
all(path, ...handlers) {
this._onListeners.push(() => {
return this._router.all(this._normalizePath(this._globalPrefix, path), this._wrapHandlers(...this._globalMiddlewares, ...handlers));
});
return this;
}
async _import(dir, pattern) {
const recursive = dir.endsWith("*");
const root = recursive
? dir.slice(0, -1)
: dir;
return this._scan(root, pattern, recursive);
}
async _scan(dir, pattern, recursive = false) {
const entries = await fs_1.default.promises.readdir(dir, {
withFileTypes: true,
});
const result = [];
for (const entry of entries) {
const fullPath = path_1.default.resolve(dir, entry.name);
if (entry.isDirectory()) {
if (recursive) {
result.push(...(await this._scan(fullPath, pattern, true)));
}
continue;
}
if (!pattern ||
pattern.test(entry.name)) {
result.push(fullPath);
}
}
return result;
}
async _registerControllers() {
if (this._controllers == null)
return;
if (!Array.isArray(this._controllers)) {
let failDir = false;
const controllers = await this._import(this._controllers.folder, this._controllers.name).catch(err => {
console.log(`\x1b[31m[ControllerLoader ERROR]\x1b[0m Failed to read directory:\n` +
`Error: ${err.message}\n`);
failDir = true;
return [];
});
if (!controllers.length && !failDir) {
console.log(`\x1b[33m[ControllerLoader Warning]\x1b[0m No controllers found in:\n` +
`\x1b[36m${this._controllers.folder}\x1b[0m\n\n` +
`Using pattern:\n` +
`\x1b[36m${this._controllers.name}\x1b[0m`);
console.log(`\nMake sure that:\n` +
`- the folder path exists\n` +
`- controller files match the pattern\n` +
`- recursive scanning is enabled for nested folders\n\n` +
`Example:\n` +
`\x1b[36m${this._controllers.folder}/*\x1b[0m\n`);
}
for (const file of controllers) {
const imported = await Promise.resolve(`${file}`).then(s => __importStar(require(s)));
let maybeController = imported?.default;
if (maybeController == null) {
const entry = Object
.entries(imported)
.find(([name]) => {
return /controller$/i.test(name);
});
maybeController = entry?.[1];
}
const controller = maybeController;
if (typeof controller !== "function") {
console.log(`\x1b[31m[ControllerLoader ERROR]\x1b[0m \x1b[36m${file}\x1b[0m must export a controller class`);
continue;
}
const controllerInstance = new controller();
const prefixPath = Reflect.getMetadata("controllers", controller) ?? '';
const routers = Reflect.getMetadata("routers", controller) ?? [];
const swaggers = Reflect.getMetadata("swaggers", controller) ?? [];
if (prefixPath == null)
continue;
for (const { method, path, handler } of Array.from(routers)) {
const find = Array.from(swaggers).find(s => s.handler === handler);
if (find != null) {
this._swaggerSpecs = [
...this._swaggerSpecs,
{
...find,
path: this._normalizePath(this._globalPrefix, prefixPath, path),
method
}
];
}
this[method](this._normalizePath(this._globalPrefix, prefixPath, path), controllerInstance[String(handler)].bind(controllerInstance));
}
}
return;
}
for (const controller of this._controllers) {
const controllerInstance = new controller();
const prefixPath = Reflect.getMetadata("controllers", controller) ?? '';
const routers = Reflect.getMetadata("routers", controller) ?? [];
const swaggers = Reflect.getMetadata("swaggers", controller) ?? [];
if (prefixPath == null)
continue;
for (const { method, path, handler } of Array.from(routers)) {
const find = Array.from(swaggers).find(s => s.handler === handler);
if (find != null) {
this._swaggerSpecs = [
...this._swaggerSpecs,
{
...find,
path: this._normalizePath(this._globalPrefix, prefixPath, path),
method
}
];
}
this[method](this._normalizePath(this._globalPrefix, prefixPath, path), controllerInstance[String(handler)].bind(controllerInstance));
}
}
}
async _registerMiddlewares() {
if (this._middlewares == null)
return;
if (!Array.isArray(this._middlewares)) {
let failDir = false;
const middlewares = await this._import(this._middlewares.folder, this._middlewares.name)
.catch(err => {
console.log(`\x1b[31m[MiddlewareLoader ERROR]\x1b[0m Failed to read directory:\n` +
`Error: ${err.message}\n`);
failDir = true;
return [];
});
if (!middlewares.length && !failDir) {
console.log(`\x1b[33m[MiddlewareLoader Warning]\x1b[0m No middlewares found in:\n` +
`\x1b[36m${this._middlewares.folder}\x1b[0m\n\n` +
`Using pattern:\n` +
`\x1b[36m${this._middlewares.name}\x1b[0m`);
console.log(`\nMake sure that:\n` +
`- the folder path exists\n` +
`- middleware files match the pattern\n` +
`- recursive scanning is enabled for nested folders\n\n` +
`Example:\n` +
`\x1b[36m${this._middlewares.folder}/*\x1b[0m\n`);
}
for (const file of middlewares) {
const imported = await Promise.resolve(`${file}`).then(s => __importStar(require(s)));
let maybeMiddleware = imported?.default;
if (maybeMiddleware == null) {
const entry = Object
.entries(imported)
.find(([name]) => {
return /middleware$/i.test(name);
});
maybeMiddleware = entry?.[1];
}
const middleware = maybeMiddleware;
if (typeof middleware !== "function") {
console.log(`\x1b[31m[MiddlewareLoader ERROR]\x1b[0m \x1b[36m${file}\x1b[0m must export a middleware`);
continue;
}
this.use(middleware);
}
return;
}
const middlewares = this._middlewares;
for (const middleware of middlewares) {
this.use(middleware);
}
return;
}
_wrapHandlers(...handlers) {
return (req, res, ps) => {
const dispatch = (index = 0) => {
const handler = handlers[index];
if (!handler)
return;
const ctx = this._createContext({ req, res, ps });
try {
const next = () => dispatch(index + 1);
const isLast = index === handlers.length - 1;
if (isLast) {
return this._wrapResponse(handler)(ctx, this._nextError(ctx));
}
return handler(ctx, next);
}
catch (err) {
return this._nextError(ctx)(err);
}
};
return dispatch();
};
}
_wrapResponse(handler) {
return (ctx, next) => {
Promise.resolve(handler(ctx, next))
.then(result => {
if (ctx.res.writableEnded) {
return;
}
if (result instanceof http_1.ServerResponse) {
return;
}
if (result instanceof stream_1.Stream) {
return;
}
if (result == null) {
ctx.res.noContent();
return;
}
if (typeof result === 'string') {
ctx.res.send(result);
return;
}
ctx.res.json(result);
return;
})
.catch(err => next(err));
};
}
_nextError(ctx) {
const NEXT_MESSAGE = "The 'next' function does not have any subsequent function.";
return (err) => {
if (ctx.res.writableEnded)
return;
const errorMessage = err?.message || NEXT_MESSAGE;
if (this._errorHandler != null) {
return this._errorHandler(err, ctx);
}
if (!ctx.res.headersSent) {
ctx.res.writeHead(500, const_1.HEADER_CONTENT_TYPES['json']);
}
if (this._formatResponse != null) {
ctx.res.end(JSON.stringify(this._formatResponse({
message: errorMessage
}, ctx.res.statusCode)));
return;
}
ctx.res.end(JSON.stringify({
message: errorMessage
}));
return;
};
}
_clusterMode({ server, port, hostname, callback }) {
if (cluster_1.default.isPrimary) {
const numCPUs = os_1.default.cpus().length;
const maxWorkers = typeof this._cluster === 'boolean' || this._cluster == null
? numCPUs
: this._cluster;
for (let i = 0; i < maxWorkers; i++) {
cluster_1.default.fork();
}
cluster_1.default.on('exit', () => {
cluster_1.default.fork();
});
}
if (cluster_1.default.isWorker) {
if (this._adapter.kind === 'uWS') {
const handler = () => {
this._onListeners.forEach(listener => listener());
if (this._swagger.use) {
this._swaggerHandler();
}
callback?.({ server, port });
};
if (hostname) {
server.listen(port, hostname, handler);
return server;
}
server.listen(port, handler);
return server;
}
const args = hostname
? [port, hostname, () => callback?.({ server, port: port })]
: [port, () => callback?.({ server, port: port })];
server.listen(...args);
server.on('listening', () => {
this._onListeners.forEach(listener => listener());
if (this._swagger.use) {
this._swaggerHandler();
}
});
server.on('error', (_) => {
port = Math.floor(Math.random() * 8999) + 1000;
server.listen(port);
});
}
return;
}
async _createServer() {
await this._registerMiddlewares();
await this._registerControllers();
const lookup = this._router.lookup.bind(this._router);
const cors = this._cors;
const adapter = this._adapter;
if (adapter.kind === 'uWS') {
const server = adapter.server.App();
server.any('/*', (uwsRes, uwsReq) => {
const { req, res } = (0, uWS_1.uWSAdaptRequestResponse)(uwsReq, uwsRes);
if (cors)
cors(req, res);
return lookup(req, res);
});
if (this._ws?.handler) {
server.ws('/*', {
open: (ws) => {
this._ws.handler?.connection?.(ws);
},
message: (ws, message) => {
this._ws.handler?.message?.(ws, Buffer.from(message));
},
close: (ws, code, message) => {
this._ws.handler?.close?.(ws, code, Buffer.from(message));
}
});
}
return server;
}
if (adapter.kind === 'net') {
const server = net_1.default.createServer((socket) => {
(0, net_2.netAdaptRequestResponse)(socket, (req, res) => {
if (cors)
cors(req, res);
return lookup(req, res);
});
});
if (this._ws?.handler) {
this._ws.server = new ws_1.default.Server({ server, ...this._ws.options });
this._ws.server.on('connection', (ws) => {
if (this._ws.handler?.connection) {
this._ws.handler.connection(ws);
}
ws.on('message', (data) => {
this._ws.handler?.message?.(ws, data);
});
ws.on('close', (code, reason) => {
if (this._ws.handler?.close) {
this._ws.handler?.close(ws, code, reason);
}
});
ws.on('error', (err) => {
if (this._ws.handler?.error) {
this._ws.handler.error(ws, err);
}
});
});
}
return server;
}
const server = http_1.default.createServer((req, res) => {
if (cors)
cors(req, res);
return lookup(req, res);
});
if (this._ws?.handler) {
this._ws.server = new ws_1.default.Server({ server, ...this._ws.options });
this._ws.server.on('connection', (ws) => {
if (this._ws.handler?.connection) {
this._ws.handler.connection(ws);
}
ws.on('message', (data) => {
this._ws.handler?.message?.(ws, data);
});
ws.on('close', (code, reason) => {
if (this._ws.handler?.close) {
this._ws.handler?.close(ws, code, reason);
}
});
ws.on('error', (err) => {
if (this._ws.handler?.error) {
this._ws.handler.error(ws, err);
}
});
});
}
return server;
}
_createContext({ req, res, ps }) {
const request = req;
const response = (0, response_1.Response)(req, res, {
formatResponse: this._formatResponse,
isUwebSocket: this._adapter.kind === 'uWS'
});
const headers = req.headers;
const params = ps;
const body = request.body;
const files = request.files;
const cookies = request.cookies;
const query = this._parser.queryString(req.url) || {};
const xff = headers['x-forwarded-for'];
const xrip = headers['x-real-ip'];
const cfip = headers['cf-connecting-ip'];
let ips = [];
if (cfip) {
ips = Array.isArray(cfip) ? cfip : [cfip];
}
else if (xff) {
ips = Array.isArray(xff) ? xff : [xff];
}
else if (xrip) {
ips = Array.isArray(xrip) ? xrip : [xrip];
}
else {
const addr = req.socket?.remoteAddress;
ips = addr ? [addr] : [];
}
const ip = (ips.length ? ips[0] : null);
request.params = params;
request.query = query;
request.ip = ip;
request.ips = ips;
return {
req: request,
res: response,
headers: headers || Object.create(null),
params: params || Object.create(null),
query,
body: body || Object.create(null),
files: files || Object.create(null),
cookies: cookies || Object.create(null),
ip,
ips
};
}
_normalizePath(...paths) {
const path = paths
.join('/')
.replace(/\/+/g, '/')
.replace(/\/+$/, '');
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
return /\/api\/api/.test(normalizedPath)
? normalizedPath.replace(/\/api\/api\//, "/api/")
: normalizedPath;
}
async _swaggerHandler() {
const routes = this.routers
.routes
.filter(r => {
return [
"GET", "POST",
"PUT", "PATCH",
"DELETE",
"HEAD", "OPTIONS"
].includes(r.method);
});
const { path, html, staticSwaggerHandler, staticUrl } = await this._parser.swagger({
...this._swagger,
specs: this._swaggerSpecs,
routes,
globalPrefix: this._globalPrefix
});
this._router.get(staticUrl, staticSwaggerHandler);
this._router.get(path, (req, res) => {
res.writeHead(200, const_1.HEADER_CONTENT_TYPES['html']);
res.end(html);
return;
});
return;
}
}
exports.Spear = Spear;
class Application extends Spear {
}
exports.Application = Application;
exports.default = Spear;
//# sourceMappingURL=index.js.map