UNPKG

restifyx.js

Version:

Advanced API endpoint handler system with automatic documentation

301 lines 16.6 kB
"use strict"; 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