restifyx.js
Version:
Advanced API endpoint handler system with automatic documentation
301 lines • 16.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.EndpointRegistry = void 0;
const glob_1 = require("glob");
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const url_1 = require("url");
const perf_hooks_1 = require("perf_hooks");
const logger_1 = require("../utils/logger");
class EndpointRegistry {
constructor(config) {
this.registeredEndpoints = [];
this.logger = (0, logger_1.getLogger)();
this.config = config;
}
/**
* This method returns the list of registered endpoints.
* @returns The list of registered endpoints
* @public
*/
getRegisteredEndpoints() {
return this.registeredEndpoints;
}
/**
* This method registers a single endpoint with the given Express app.
* @param app - The Express app to register the endpoint with
* @param endpoint - The endpoint metadata to register
* @throws Will throw an error if the endpoint is invalid or if the HTTP method is not supported
* @public
*/
registerSingleEndpoint(app, endpoint) {
if (!endpoint.path || !endpoint.method || !endpoint.handler) {
throw new Error(`Invalid endpoint configuration: missing required properties`);
}
const method = endpoint.method.toLowerCase();
if (!app[method]) {
throw new Error(`Invalid HTTP method: ${endpoint.method}`);
}
;
app[method](endpoint.path, this.createDebugMiddleware(endpoint), ...(endpoint.middleware || []), async (req, res, next) => {
try {
await endpoint.handler(req, res, next);
}
catch (error) {
next(error);
}
});
this.logger.debug(`Registered endpoint: ${endpoint.method} ${endpoint.path}`);
this.registeredEndpoints.push(endpoint);
}
/**
* This method registers all endpoints found in the specified directory.
* @param app - The Express app to register the endpoints with
* @returns An object containing the registration result
* @throws Will throw an error if the registration fails
* @public
*/
async registerEndpoints(app) {
try {
const endpointsDir = path_1.default.resolve(process.cwd(), this.config.endpoints.directory);
if (!fs_1.default.existsSync(endpointsDir)) {
this.logger.warn(`Endpoints directory not found: ${endpointsDir}. Creating it...`);
fs_1.default.mkdirSync(endpointsDir, { recursive: true });
}
const pattern = `${this.config.endpoints.directory.replace(/\\/g, "/")}/${this.config.endpoints.pattern}`;
this.logger.debug(`Using glob pattern: ${pattern}`);
const endpointFiles = await (0, glob_1.glob)(pattern);
if (endpointFiles.length === 0) {
this.logger.warn(`No endpoint files found matching pattern: ${pattern}`);
}
const validEndpoints = [];
const failedEndpoints = [];
for (const file of endpointFiles) {
try {
const absolutePath = path_1.default.resolve(process.cwd(), file);
this.logger.debug(`Loading endpoint from: ${absolutePath}`);
let endpointModule = null;
let loadError = null;
try {
endpointModule = require(absolutePath);
this.logger.debug(`Loaded endpoint using require(): ${file}`);
}
catch (err) {
loadError = err;
this.logger.debug(`Failed to load with require: ${err instanceof Error ? err.message : String(err)}`);
try {
const fileUrl = (0, url_1.pathToFileURL)(absolutePath).href;
endpointModule = await import(fileUrl);
this.logger.debug(`Loaded endpoint using import(): ${file}`);
loadError = null;
}
catch (importErr) {
this.logger.debug(`Failed to load with import: ${importErr instanceof Error ? importErr.message : String(importErr)}`);
try {
const relativePath = path_1.default.relative(__dirname, absolutePath);
endpointModule = await import(relativePath);
this.logger.debug(`Loaded endpoint using relative import: ${file}`);
loadError = null;
}
catch (relImportErr) {
this.logger.debug(`Failed to load with relative import: ${relImportErr instanceof Error ? relImportErr.message : String(relImportErr)}`);
}
}
}
if (!endpointModule && loadError) {
throw loadError;
}
this.logger.debug(`Module type: ${typeof endpointModule}`);
if (endpointModule) {
this.logger.debug(`Module keys: ${Object.keys(endpointModule).join(", ")}`);
if (endpointModule.default) {
this.logger.debug(`Default export type: ${typeof endpointModule.default}`);
if (typeof endpointModule.default === "object") {
this.logger.debug(`Default export keys: ${Object.keys(endpointModule.default).join(", ")}`);
}
}
}
let endpoint = null;
if (this.isValidEndpoint(endpointModule)) {
endpoint = endpointModule;
this.logger.debug(`Using module.exports as endpoint metadata for ${file}`);
}
else if (endpointModule.default && this.isValidEndpoint(endpointModule.default)) {
endpoint = endpointModule.default;
this.logger.debug(`Using default export as endpoint metadata for ${file}`);
}
else if (this.isEndpointLike(endpointModule)) {
try {
endpoint = endpointModule.build();
this.logger.debug(`Built endpoint from module.exports for ${file}`);
}
catch (buildError) {
throw new Error(`Failed to build endpoint from ${file}: ${buildError instanceof Error ? buildError.message : String(buildError)}`);
}
}
else if (endpointModule.default && this.isEndpointLike(endpointModule.default)) {
try {
endpoint = endpointModule.default.build();
this.logger.debug(`Built endpoint from default export for ${file}`);
}
catch (buildError) {
throw new Error(`Failed to build endpoint from ${file}: ${buildError instanceof Error ? buildError.message : String(buildError)}`);
}
}
if (!endpoint) {
if (typeof endpointModule === "function") {
try {
const result = endpointModule();
if (this.isValidEndpoint(result)) {
endpoint = result;
this.logger.debug(`Using result of function call as endpoint for ${file}`);
}
else if (this.isEndpointLike(result)) {
endpoint = result.build();
this.logger.debug(`Built endpoint from function call result for ${file}`);
}
}
catch (funcError) {
this.logger.debug(`Failed to call exported function: ${funcError instanceof Error ? funcError.message : String(funcError)}`);
}
}
else if (typeof endpointModule.default === "function") {
try {
const result = endpointModule.default();
if (this.isValidEndpoint(result)) {
endpoint = result;
this.logger.debug(`Using result of default function call as endpoint for ${file}`);
}
else if (this.isEndpointLike(result)) {
endpoint = result.build();
this.logger.debug(`Built endpoint from default function call result for ${file}`);
}
}
catch (funcError) {
this.logger.debug(`Failed to call default exported function: ${funcError instanceof Error ? funcError.message : String(funcError)}`);
}
}
}
if (!endpoint) {
throw new Error(`Could not find a valid endpoint in ${file}. Make sure you're exporting correctly:\n` +
`- For CommonJS: module.exports = createEndpoint()...\n` +
`- For ES modules: export default createEndpoint()...\n` +
`The exported object should be the result of createEndpoint() with all required methods called.`);
}
endpoint.filePath = file;
this.registerSingleEndpoint(app, endpoint);
validEndpoints.push(endpoint);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error(`Error registering endpoint from ${file}: ${errorMessage}`, { error });
failedEndpoints.push({ path: file, error: errorMessage });
if (this.config.debug) {
try {
const fileContent = fs_1.default.readFileSync(file, "utf8");
const lines = fileContent.split("\n");
const errorKey = errorMessage.split(":")[0];
const errorLine = lines.findIndex((line) => line.includes(errorKey));
if (errorLine >= 0) {
this.logger.error(`Error in ${file} line: ${errorLine + 1}`);
this.logger.error(`Tip: ${this.getTipForError(errorMessage)}`);
}
this.logger.debug(`File content of ${file}:`);
this.logger.debug(fileContent);
}
catch (readError) {
this.logger.error(`Could not read file for debugging: ${file}`);
}
}
}
}
if (failedEndpoints.length > 0) {
return {
success: validEndpoints.length > 0,
endpoints: validEndpoints,
error: `Failed to register ${failedEndpoints.length} endpoint(s)`,
failedEndpoints,
};
}
return { success: true, endpoints: validEndpoints };
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.logger.error("Failed to register endpoints", { error });
return { success: false, endpoints: [], error: errorMessage };
}
}
isValidEndpoint(obj) {
return (obj &&
typeof obj === "object" &&
typeof obj.path === "string" &&
typeof obj.method === "string" &&
typeof obj.handler === "function");
}
isEndpointLike(obj) {
return (obj &&
typeof obj === "object" &&
typeof obj.build === "function" &&
typeof obj.setName === "function" &&
typeof obj.setPath === "function" &&
typeof obj.setMethod === "function" &&
typeof obj.setHandler === "function");
}
createDebugMiddleware(endpoint) {
return (req, res, next) => {
if (!endpoint.options?.logging && !this.config.debug)
return next();
const startTime = perf_hooks_1.performance.now();
const originalEnd = res.end;
res.end = function (chunk, encoding, cb) {
const endTime = perf_hooks_1.performance.now();
const duration = (endTime - startTime).toFixed(2);
let responseBody = "";
if (chunk) {
responseBody = chunk.toString();
try {
const parsed = JSON.parse(responseBody);
responseBody = JSON.stringify(parsed);
}
catch {
}
}
(0, logger_1.getLogger)().info(`[${endpoint.method} - ${endpoint.path}] ${req.method} ${req.originalUrl} | ${duration}ms` +
(Object.keys(req.params).length ? ` | Params: ${JSON.stringify(req.params)}` : "") +
(Object.keys(req.query).length ? ` | Query: ${JSON.stringify(req.query)}` : "") +
(req.body && Object.keys(req.body).length ? ` | Body: ${JSON.stringify(req.body)}` : "") +
` >> ${responseBody.length > 100 ? responseBody.substring(0, 100) + "..." : responseBody}`);
return originalEnd.call(this, chunk, encoding, cb);
};
next();
};
}
getTipForError(errorMessage) {
if (errorMessage.includes("Cannot find module"))
return "Make sure the imported module exists and is correctly installed. Check file path and ensure no special characters in the path.";
if (errorMessage.includes("is not a function"))
return "Check that you are calling a valid function and that the object is properly initialized";
if (errorMessage.includes("is not defined"))
return "Ensure the variable is declared before use and is in scope";
if (errorMessage.includes("Invalid HTTP method"))
return "Use one of the valid HTTP methods: GET, POST, PUT, DELETE, PATCH";
if (errorMessage.includes("SyntaxError"))
return "Check your syntax for errors like missing brackets or semicolons";
if (errorMessage.includes("export"))
return "Make sure you are exporting your endpoint correctly with export default or module.exports = ...";
if (errorMessage.includes("Unexpected token"))
return "Check for syntax errors. If using ES modules, ensure your environment supports them.";
if (errorMessage.includes("Invalid endpoint format") || errorMessage.includes("Could not find a valid endpoint"))
return "Make sure you're exporting the endpoint correctly. For CommonJS use: module.exports = createEndpoint()... For ES modules use: export default createEndpoint()...";
if (errorMessage.includes("No default export found"))
return "For CommonJS, use module.exports = createEndpoint()... instead of module.exports.default = ...";
if (errorMessage.includes("Failed to build endpoint"))
return "There was an error building your endpoint. Check that all required methods are called (setPath, setMethod, setHandler).";
return "Check your syntax and ensure all required properties are defined";
}
}
exports.EndpointRegistry = EndpointRegistry;
//# sourceMappingURL=EndpointRegistry.js.map