tspace-spear
Version:
tspace-spear is a lightweight API framework for Node.js that is fast and highly focused on providing the best developer experience. It utilizes the native HTTP server
452 lines • 20.4 kB
JavaScript
;
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 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 querystring_1 = __importDefault(require("querystring"));
const string_decoder_1 = require("string_decoder");
const busboy_1 = __importDefault(require("busboy"));
class ParserFactory {
files(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 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: String(mime_types_1.default.extension(String(mimeType))),
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);
});
}
body(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', () => {
try {
const isUrlEncoded = req.headers['content-type']?.includes('x-www-form-urlencoded');
if (isUrlEncoded) {
return resolve(querystring_1.default.parse(payload));
}
payload += decoder.end();
return resolve(JSON.parse(payload));
}
catch (e) {
return resolve({});
}
});
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;
}
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 specPaths = (routes) => {
let paths = {};
for (const r of routes) {
if (r.path === '*')
continue;
const path = r.path.replace(/:(\w+)/g, "{$1}");
const method = r.method.toLocaleLowerCase();
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
};
}
const swagger = doc.options.find(s => (s.path === r.path) && (s.method.toLocaleLowerCase() === method));
if (swagger != null) {
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 (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) {
spec.parameters = [
...spec.parameters,
...Object.entries(swagger.query).map(([k, v]) => {
return {
name: k,
in: "query",
required: v.required == null ? false : true,
schema: {
type: v.type
}
};
})
];
}
if (swagger.cookies != null) {
spec.parameters = [
...spec.parameters,
...[{
name: "Cookie",
in: "header",
required: swagger.cookies.required == null ? false : 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 == null ? "description" : swagger.body.description,
required: swagger.body?.required == null ? false : true,
content: {
"application/json": {
schema: {
type: "object",
properties: swagger.body.properties
}
}
}
};
}
if (swagger.files != null) {
spec.requestBody = {
description: swagger.files?.description == null ? "description" : swagger.files.description,
required: swagger.files?.required == null ? false : true,
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: swagger.files.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
};
}
paths[path][method] = spec;
continue;
}
spec.tags = [
tags == null || tags === '' || /^:[^:]*$/.test(tags) ? 'default' : tags
];
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"
}
};
});
}
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}" />
</head>
<body>
<div id="swagger-ui"></div>
</body>
<script src="${scriptBundle}"></script>
<script src="${scriptStandalonePreset}"></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({ spec : ${JSON.stringify(spec)} ,
dom_id: '#swagger-ui',
withCredentials: true,
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset], layout: "StandaloneLayout"});
};
</script>
</html>
`;
const staticSwaggerHandler = (req, res, params) => {
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';
try {
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': contentType });
return res.end(`The file '${requestedFilePath}' is not exists`);
}
};
return {
path: doc.path,
staticUrl: `${STATIC_URL}/*`,
staticSwaggerHandler,
html
};
}
}
exports.ParserFactory = ParserFactory;
//# sourceMappingURL=parser-factory.js.map