UNPKG

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.

793 lines 35 kB
"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.ParserFactory = void 0; const busboy_1 = __importDefault(require("busboy")); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const mime_types_1 = __importDefault(require("mime-types")); const crypto_1 = __importDefault(require("crypto")); const swagger_ui_dist_1 = __importDefault(require("swagger-ui-dist")); const string_decoder_1 = require("string_decoder"); const fast_querystring_1 = __importDefault(require("fast-querystring")); const utils_1 = require("../utils"); const uWS_1 = require("./uWS"); const net_1 = require("./net"); class ParserFactory { uWebStockets = false; net = false; useAdater(adapter) { if (adapter.kind === 'uWS') { this.uWebStockets = true; } if (adapter.kind === 'net') { this.net = true; } return this; } queryString(url) { const i = url.indexOf("?"); if (i === -1) return {}; return fast_querystring_1.default.parse(url.slice(i + 1)); } async files({ req, res, options, }) { if (this.uWebStockets) { return (0, uWS_1.uWSfiles)({ req, res, options }); } if (this.net) { return (0, net_1.netFiles)(req, options); } const temp = options.tempFileDir; if (!fs_1.default.existsSync(temp)) { try { fs_1.default.mkdirSync(temp, { recursive: true }); } catch (err) { } } return new Promise((resolve, reject) => { const body = {}; const files = {}; const fileWritePromises = []; const bb = (0, busboy_1.default)({ headers: req.headers, defParamCharset: "utf8" }); const removeTemp = (fileTemp, ms) => { const remove = () => { try { fs_1.default.unlinkSync(fileTemp); } catch (err) { } }; setTimeout(remove, ms); }; bb.on("file", (fieldName, fileData, info) => { const { filename, mimeType } = info; const extension = mime_types_1.default.extension(mimeType) || path_1.default.extname(filename).replace(".", "") || "bin"; const tempFilename = crypto_1.default.randomBytes(16).toString("hex"); const filePath = path_1.default.join(path_1.default.resolve(), `${temp}/${tempFilename}`); const writeStream = fs_1.default.createWriteStream(filePath); let fileSize = 0; fileData.on("data", (data) => { fileSize += data.length; if (fileSize > options.limit) { fileData.unpipe(writeStream); writeStream.destroy(); return reject(new Error(`The file '${fieldName}' is too large to be uploaded. The limit is '${options.limit}' bytes.`)); } }); const fileWritePromise = new Promise((resolve, reject) => { fileData.pipe(writeStream); writeStream.on("finish", () => { const file = { name: filename, tempFilePath: filePath, tempFileName: tempFilename, mimetype: mimeType, extension: extension, size: fileSize, sizes: { bytes: fileSize, kb: fileSize / 1024, mb: fileSize / 1024 / 1024, gb: fileSize / 1024 / 1024 / 1024, }, write: (to) => { return new Promise((resolve, reject) => { fs_1.default .createReadStream(filePath) .pipe(fs_1.default.createWriteStream(to)) .on("finish", () => { return resolve(null); }) .on("error", (err) => { return reject(err); }); }); }, remove: () => { return new Promise((resolve) => setTimeout(() => { fs_1.default.unlinkSync(filePath); return resolve(null); }, 100)); }, }; if (files[fieldName] == null) { files[fieldName] = []; } files[fieldName].push(file); if (options.removeTempFile.remove) { removeTemp(filePath, options.removeTempFile.ms); } return resolve(null); }); writeStream.on("error", reject); }); fileWritePromises.push(fileWritePromise); }); bb.on("field", (name, value) => { body[name] = value; }); bb.on("finish", () => { Promise.all(fileWritePromises) .then(() => { return resolve({ files, body, }); }) .catch((err) => { return reject(err); }); }); bb.on("error", (err) => { return reject(err); }); req.pipe(bb); }); } async body(req, res) { if (this.uWebStockets) { return (await (0, uWS_1.uWSBody)(req, res)); } if (this.net) { return (await (0, net_1.netBody)(req)); } return new Promise((resolve, reject) => { const decoder = new string_decoder_1.StringDecoder("utf-8"); let payload = ""; req.on("data", (data) => { payload += decoder.write(data); }); req.on("end", async () => { payload += decoder.end(); const contentType = req.headers["content-type"]?.toLowerCase() || null; try { const body = await (0, utils_1.normalizeRequestBody)({ contentType, payload }); return resolve(body); } catch (err) { return reject(err); } }); req.on("error", (err) => { return reject(err); }); }); } cookies(req) { const cookies = {}; const cookieString = req.headers?.cookie; if (cookieString == null) return null; for (const cookie of cookieString.split(";")) { const [name, value] = cookie.split("=").map((v) => v.trim()); cookies[name] = decodeURIComponent(value); } for (const name of Object.keys(cookies)) { const cookie = cookies[name]; if (!cookie.startsWith("Expires=")) continue; const expiresString = cookie.replace("Expires=", ""); const expiresDate = new Date(expiresString); if (isNaN(expiresDate.getTime()) || expiresDate < new Date()) { delete cookies[name]; } } return cookies; } async swagger(doc) { const spec = { openapi: "3.1.0", info: doc.info ?? { title: "API Documentation", description: "Documentation", version: "1.0.0", }, components: { securitySchemes: { BearerToken: { type: "http", scheme: "bearer", name: "Authorization", description: "Enter your token in the format : 'Bearer {TOKEN}'", }, cookies: { type: "apiKey", in: "header", name: "Cookie", description: "Enter your cookies in the headers", }, }, }, servers: doc.servers, tags: doc.tags, paths: {}, }; const { appRoutes } = await Promise.resolve().then(() => __importStar(require("../compiler/pre-routes"))); const specPaths = (routes) => { let paths = {}; const defaultSpecResponse = { "200": { description: "OK", content: { "application/json": { schema: { type: "object", properties: { message: { example: "success", }, }, }, }, }, }, }; for (const r of routes) { if (r.path === "*") continue; const path = r.path.replace(/:(\w+)/g, "{$1}"); const method = r.method.toLowerCase(); const pathWithoutGlobalPrefix = r.path.replace(`/${doc.globalPrefix}/`, "/"); //@ts-ignore const preRoute = appRoutes[pathWithoutGlobalPrefix]?.[r.method]; const swagger = (doc.specs ?? []).find((s) => { return s.path === r.path && s.method.toLowerCase() === method; }); const decoratedOnly = doc.options?.decoratedOnly ?? false; if ((swagger == null && decoratedOnly) || swagger?.disabled) { continue; } if (paths[path] == null) { paths[path] = { [method]: {}, }; } const spec = {}; const tags = /\/api\/v\d+/.test(r.path) ? r.path.split("/")[3] : /\/api/.test(r.path) ? r.path.split("/")[2] : r.path.split("/")[1]; spec.parameters = []; spec.responses = {}; spec.tags = []; if (doc.responses != null) { const responses = {}; for (const response of Array.from(doc.responses ?? [])) { if (response == null || !Object.keys(response).length) continue; responses[`${response.status}`] = { description: response.description, content: { "application/json": { schema: { type: "object", properties: response.example == null ? {} : Object.keys(response.example).reduce((prev, key) => { prev[key] = { example: (response?.example ?? {})[key] ?? {}, }; return prev; }, {}), }, }, }, }; } spec.responses = { ...responses, }; } if (swagger == null) { spec.tags = [ tags == null || tags === "" || /^:[^:]*$/.test(tags) ? "default" : tags, ]; if (!Object.keys(spec.responses).length) { spec.responses = defaultSpecResponse; } if (preRoute && Object.keys(preRoute.params ?? {}).length) { const queryParams = Object.entries(preRoute.params ?? {}).map(([k, v]) => { return { name: k, in: "path", required: false, description: `Params for '${k}'`, example: v, schema: { type: v } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...queryParams, ]; } else if (Array.isArray(r.params) && Array.from(r.params).length) { spec.parameters = Array.from(r.params).map((p) => { return { name: p, in: "path", required: true, schema: { type: "string", }, }; }); } if (preRoute && Object.keys(preRoute.query ?? {}).length) { const queryParams = Object.entries(preRoute.query ?? {}).map(([k, v]) => { return { name: k, in: "query", required: false, description: `QueryParams for '${k}'`, example: v, schema: { type: v, } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...queryParams, ]; } if (preRoute && Object.keys(preRoute.body ?? {}).length) { const properties = Object.fromEntries(Object.entries(preRoute.body ?? {}).map(([key, value]) => [ key, { type: value, example: value, }, ])); spec.requestBody = { description: `Body payload`, required: false, content: { "application/json": { schema: { type: "object", properties: properties, required: false } }, }, }; } if (preRoute && Object.keys(preRoute.files ?? {}).length) { const properties = Object.fromEntries(Object.entries(preRoute.files ?? {}).map(([key, value]) => [ key, { type: value, format: "binary", example: value, }, ])); spec.requestBody = { description: `File Upload payload`, required: false, content: { "multipart/form-data": { schema: { type: "object", properties: properties, }, }, }, }; } if (preRoute && Object.keys(preRoute.response ?? {}).length) { const responses = {}; responses["200"] = { description: null, content: { "application/json": { schema: { type: "object", properties: Object.keys(preRoute.response ?? {}).reduce((prev, key) => { prev[key] = { example: (preRoute.response ?? {})[key] ?? {}, }; return prev; }, {}) }, }, }, }; spec.responses = { ...responses, }; } paths[path][method] = spec; continue; } /** Load from Swagger */ spec.tags = [ swagger.tags == null ? tags == null || tags === "" || /^:[^:]*$/.test(tags) ? "default" : tags : swagger.tags, ]; if (swagger.bearerToken) { spec.security = [{ BearerToken: [] }]; } if (swagger.summary != null) { spec.summary = swagger.summary; } if (swagger.description != null) { spec.description = swagger.description; } if (swagger.params != null) { const params = Object.entries(swagger.params).map(([k, v]) => { return { name: k, in: "path", required: v?.required === true, description: v?.description, example: v?.example || v?.enum, schema: { type: v?.type ?? "string", } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...params, ]; } else if (preRoute && Object.keys(preRoute.params ?? {}).length) { const queryParams = Object.entries(preRoute.params ?? {}).map(([k, v]) => { return { name: k, in: "path", required: false, description: `Params for '${k}'`, example: v, schema: { type: v } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...queryParams, ]; } else if (Array.isArray(r.params) && Array.from(r.params).length) { spec.parameters = Array.from(r?.params).map((p) => { return { name: p, in: "path", required: true, schema: { type: "string", }, }; }); } if (swagger.query != null) { const queryParams = Object.entries(swagger.query).map(([k, v]) => { return { name: k, in: "query", required: v?.required === true, description: v?.description, example: v?.example || v?.enum, schema: { type: v?.type ?? "string", } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...queryParams, ]; } else if (preRoute && Object.keys(preRoute.query ?? {}).length) { const queryParams = Object.entries(preRoute.query ?? {}).map(([k, v]) => { return { name: k, in: "query", required: false, description: `QueryParams for '${k}'`, example: v, schema: { type: v, } }; }); spec.parameters = [ ...(spec.parameters ?? []), ...queryParams, ]; } if (swagger.cookies != null) { spec.parameters = [ ...spec.parameters, ...[ { name: "Cookie", in: "header", required: swagger.cookies.required === true, schema: { type: "string", }, example: swagger.cookies.names .map((v, i) => `${v}={value${i + 1}}`) .join(" ; "), description: swagger.cookies?.description, }, ], ]; } if (swagger.body != null) { spec.requestBody = { description: swagger.body.description, required: swagger.body?.required === true, content: { "application/json": { schema: { type: "object", properties: swagger.body.properties, required: Object.entries(swagger.body.properties) .filter(([_, v]) => v.required) .map(([key]) => key) }, }, }, }; } else if (preRoute && Object.keys(preRoute.body ?? {}).length) { const properties = Object.fromEntries(Object.entries(preRoute.body ?? {}).map(([key, value]) => [ key, { type: value, example: value, }, ])); spec.requestBody = { description: `Body payload`, required: false, content: { "application/json": { schema: { type: "object", properties: properties, required: false } }, }, }; } if (swagger.files != null) { spec.requestBody = { description: swagger.files.description, required: swagger.files?.required === true, content: { "multipart/form-data": { schema: { type: "object", properties: swagger.files.properties, }, }, }, }; } else if (preRoute && Object.keys(preRoute.files ?? {}).length) { const properties = Object.fromEntries(Object.entries(preRoute.files ?? {}).map(([key, value]) => [ key, { type: value, format: "binary", example: value, }, ])); spec.requestBody = { description: `File Upload payload`, required: false, content: { "multipart/form-data": { schema: { type: "object", properties: properties, }, }, }, }; } if (swagger.responses != null) { const responses = {}; for (const response of swagger.responses) { if (response == null || !Object.keys(response).length) continue; responses[`${response.status}`] = { description: response.description, content: { "application/json": { schema: { type: "object", properties: response.example == null ? {} : Object.keys(response.example).reduce((prev, key) => { prev[key] = { example: (response?.example ?? {})[key] ?? {}, }; return prev; }, {}), }, }, }, }; } spec.responses = { ...responses, }; } else if (preRoute && Object.keys(preRoute.response ?? {}).length) { const responses = {}; responses["200"] = { description: null, content: { "application/json": { schema: { type: "object", properties: Object.keys(preRoute.response ?? {}).reduce((prev, key) => { prev[key] = { example: (preRoute.response ?? {})[key] ?? {}, }; return prev; }, {}) }, }, }, }; spec.responses = { ...responses }; } if (!Object.keys(spec.responses).length) { spec.responses = defaultSpecResponse; } paths[path][method] = spec; } return paths; }; spec.paths = specPaths(doc.routes ?? []); const 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; }; const STATIC_URL = "/swagger-ui"; const iconURL = normalizePath(doc.staticUrl ?? "", `${STATIC_URL}/favicon-32x32.png`).replace(/^\/(http[s]?:\/{0,2})/, "$1"); const cssURL = normalizePath(doc.staticUrl ?? "", `${STATIC_URL}/swagger-ui.css`).replace(/^\/(http[s]?:\/{0,2})/, "$1"); const scriptBundle = normalizePath(doc.staticUrl ?? "", `${STATIC_URL}/swagger-ui-bundle.js`).replace(/^\/(http[s]?:\/{0,2})/, "$1"); const scriptStandalonePreset = normalizePath(doc.staticUrl ?? "", `${STATIC_URL}/swagger-ui-standalone-preset.js`).replace(/^\/(http[s]?:\/{0,2})/, "$1"); const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="SwaggerUI" /> <title>SwaggerUI</title> <link rel="icon" href="${iconURL}"> <link rel="stylesheet" href="${cssURL}" /> <style> .swagger-ui .topbar .download-url-wrapper { visibility: hidden; } </style> </head> <body> <div id="swagger-ui"></div> </body> <script src="${scriptBundle}"></script> <script src="${scriptStandalonePreset}"></script> <script> window.onload = () => { window.ui = SwaggerUIBundle({ dom_id: '#swagger-ui', presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], spec : ${JSON.stringify(spec)}, withCredentials: ${doc.options?.withCredentials ?? "true"}, layout: "${doc.options?.layout ?? "StandaloneLayout"}", filter: "${doc.options?.filter ?? "false"}", docExpansion: "${doc.options?.docExpansion ?? "list"}", deepLinking: "${doc.options?.deepLinking ?? "true"}", displayOperationId: "${doc.options?.displayOperationId ?? "false"}", displayRequestDuration: "${doc.options?.displayRequestDuration ?? "false"}" }); }; </script> </html> `; const staticSwaggerHandler = (req, res, params) => { try { const swaggerUiPath = swagger_ui_dist_1.default.getAbsoluteFSPath(); const mimeTypes = { ".html": "text/html", ".css": "text/css", ".js": "application/javascript", ".png": "image/png", ".jpg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".json": "application/json", }; const requestedFilePath = params["*"]; const filePath = path_1.default.join(swaggerUiPath, requestedFilePath); const extname = path_1.default.extname(filePath); const contentType = mimeTypes[extname] || "text/html"; const content = fs_1.default.readFileSync(filePath); res.writeHead(200, { "Content-Type": contentType }); return res.end(content, "utf-8"); } catch (err) { res.writeHead(404, { "Content-Type": "text/plain" }); return res.end("Not found"); } }; return { path: doc.path, staticUrl: `${STATIC_URL}/*`, staticSwaggerHandler, html, }; } } exports.ParserFactory = ParserFactory; //# sourceMappingURL=parser-factory.js.map