apiver
Version:
Advanced API Versioning Without Duplication - Git-like CLI tool for managing multiple API versions in a single codebase
155 lines (133 loc) • 6.04 kB
JavaScript
const { loadVersion, getCachedVersions } = require("./lib/runtimeLoader");
const express = require("express");
const versionRouterCache = {};
// Supported HTTP verbs
const HTTP_METHODS = ["get", "post", "put", "delete", "patch", "all"];
/**
* Middleware to dynamically serve versioned APIs from memory.
* Can auto-mount routes or use custom handler (controller function or router).
*/
function versionMiddleware(allowedVersions, handler = null) {
// Check if versions are pre-loaded at startup
const cachedVersions = getCachedVersions();
const missingVersions = allowedVersions.filter(v => !cachedVersions.includes(v));
if (missingVersions.length > 0) {
console.warn(`[APIver] Versions not pre-loaded: ${missingVersions.join(', ')}. Call loadVersion(['${allowedVersions.join("', '")}']) at startup for better performance.`);
}
return (req, res, next) => {
// Prefer Express route param when mounted as /api/:version
let version = req.params && req.params.version;
// Fallback: extract from path when not mounted that way
if (!version) {
const match = /^\/(v[0-9])(\/.*)?$/.exec(req.path);
if (!match) return res.status(400).send('Missing API version in path');
version = match[1];
// Strip the /vX prefix only in this scenario
req.url = req.url.replace(`/${version}`, '') || '/';
}
if (!allowedVersions.includes(version)) {
return res.status(400).send('Invalid API version');
}
// Get cached version data
let versionedCode;
try {
versionedCode = loadVersion(version); // This will use cache if available
} catch (err) {
return res.status(500).send(`Failed to load ${version}: ${err.message}`);
}
// If custom handler provided, use it directly
if (handler) {
req.apiVersion = version;
req.versionedCode = versionedCode;
if (typeof handler === 'function') {
// Controller function
return handler(req, res, next);
} else if (handler && typeof handler === 'object') {
// Express Router
return handler(req, res, next);
}
}
// Auto-mount from routes/ folder
if (!versionRouterCache[version]) {
try {
const codeTree = versionedCode; // Use already loaded data
const router = express.Router();
Object.keys(codeTree).forEach(filePath => {
if (filePath.startsWith("routes/") && filePath.endsWith(".js")) {
const routePath = "/" + filePath.replace(/^routes\//, "").replace(/\.js$/, "");
const routeModule = codeTree[filePath];
if (typeof routeModule === "function") {
// Default GET handler
router.get(routePath, routeModule);
} else if (typeof routeModule === "string") {
// Express Router as string - need to execute and mount
try {
// Create a safe execution context
const moduleContext = {
module: { exports: {} },
exports: {},
require: (modulePath) => {
if (modulePath === 'express') {
return express;
}
// Handle controller requires
if (modulePath.startsWith('../controllers/')) {
const controllerPath = modulePath.replace('../', '') + '.js';
const controllerCode = codeTree[controllerPath];
if (controllerCode) {
return controllerCode;
}
}
// Handle direct controller requires
if (modulePath.startsWith('./') || modulePath.startsWith('../')) {
const cleanPath = modulePath.replace(/^\.\.?\//, '') + '.js';
if (codeTree[cleanPath]) {
return codeTree[cleanPath];
}
}
console.warn(`Module ${modulePath} not found in cache`);
return {};
}
};
// Execute the router code in context
const func = new Function('module', 'exports', 'require', routeModule);
func(moduleContext.module, moduleContext.exports, moduleContext.require);
const expressRouter = moduleContext.module.exports;
// Mount the Express router
if (expressRouter && typeof expressRouter === 'function') {
router.use(routePath, expressRouter);
}
} catch (err) {
console.error(`Failed to load Express router ${filePath}:`, err.message);
}
} else if (typeof routeModule === "object" && routeModule !== null) {
// Method-based export detection (legacy format)
HTTP_METHODS.forEach(method => {
if (typeof routeModule[method] === "function") {
router[method](routePath, routeModule[method]);
}
});
// Optional custom registration
if (typeof routeModule.register === "function") {
routeModule.register(router, routePath);
}
}
}
});
versionRouterCache[version] = router;
} catch (err) {
return res.status(500).send(`Failed to create router for ${version}: ${err.message}`);
}
}
return versionRouterCache[version](req, res, next);
};
}
/**
* Clear version router cache (useful for testing)
*/
function clearCache() {
Object.keys(versionRouterCache).forEach(key => {
delete versionRouterCache[key];
});
}
module.exports = { versionMiddleware, clearCache };