diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..2720dd6 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,101 @@ +/** + * Represents a route endpoint configuration for middleware matching. + */ +export interface RouteEndpoint { + /** HTTP methods to match. Defaults to ["GET"] if not specified. */ + methods?: string[]; + /** URL pattern to match against. Supports find-my-way route patterns. */ + url: string; + /** Optional version constraint for the route. */ + version?: string; + /** Whether to update req.params with matched route parameters. Defaults to false. */ + updateParams?: boolean; +} + +/** + * Configuration options for middleware execution conditions. + */ +export interface MiddlewareOptions { + /** Array of endpoints (strings or RouteEndpoint objects) to match against. */ + endpoints?: (string | RouteEndpoint)[]; + /** Custom function to determine if middleware should execute. */ + custom?: (req: any) => boolean; +} + +/** + * Standard Express/Connect-style middleware function signature. + * @param req - The request object + * @param res - The response object + * @param next - Function to call the next middleware in the chain + */ +export type MiddlewareFunction = (req: any, res: any, next: () => void) => void; + +/** + * Enhanced middleware function with conditional execution capabilities. + * Extends the base middleware function with iff and unless methods. + */ +export interface ExtendedMiddleware extends MiddlewareFunction { + /** + * Execute middleware only if the specified condition is met. + * @param options - Condition options: MiddlewareOptions object, custom function, or array of endpoints + * @returns New ExtendedMiddleware instance with the condition applied + */ + iff: (options: MiddlewareOptions | ((req: any) => boolean) | (string | RouteEndpoint)[]) => ExtendedMiddleware; + + /** + * Execute middleware unless the specified condition is met. + * @param options - Condition options: MiddlewareOptions object, custom function, or array of endpoints + * @returns New ExtendedMiddleware instance with the condition applied + */ + unless: (options: MiddlewareOptions | ((req: any) => boolean) | (string | RouteEndpoint)[]) => ExtendedMiddleware; +} + +/** + * Configuration options for the router instance. + */ +export interface RouterOptions { + /** Default route handler function. */ + defaultRoute?: (req: any, res: any) => boolean; + /** Additional router-specific options. */ + [key: string]: any; +} + +/** + * Factory function for creating router instances. + * @param options - Optional router configuration + * @returns Router instance + */ +export interface RouterFactory { + (options?: RouterOptions): any; +} + +/** + * Main middleware enhancement function that adds iff/unless capabilities to middleware. + * + * @param routerOpts - Optional router configuration options + * @param routerFactory - Optional router factory function (defaults to find-my-way) + * @returns Function that takes a middleware and returns an ExtendedMiddleware with iff/unless methods + * + * @example + * ```typescript + * import iffUnless from 'middleware-if-unless'; + * + * const iu = iffUnless(); + * const middleware = (req, res, next) => { + * console.log('Middleware executed'); + * next(); + * }; + * + * const enhanced = iu(middleware); + * + * // Execute only for specific routes + * app.use(enhanced.iff(['/api/*'])); + * + * // Execute unless specific routes match + * app.use(enhanced.unless(['/public/*'])); + * ``` + */ +export default function iffUnless( + routerOpts?: RouterOptions, + routerFactory?: RouterFactory +): (middleware: MiddlewareFunction) => ExtendedMiddleware; diff --git a/index.js b/index.js index 1fb8477..29fe4ba 100644 --- a/index.js +++ b/index.js @@ -1,65 +1,95 @@ -const handlers = { - match: updateParams => (req, res, params) => { - if (updateParams) { - req.params = params - } +// Optimized handlers with minimal allocations +const createMatchHandler = (updateParams) => + updateParams + ? (req, res, params) => { + req.params = params + return true + } + : () => true + +const defaultHandler = () => false - return true - }, - default: () => false +// Router cache for reusing router instances +const routerCache = new WeakMap() + +function normalizeEndpoint (endpoint) { + if (typeof endpoint === 'string') { + return { url: endpoint, methods: ['GET'], updateParams: false } + } + return { + methods: endpoint.methods || ['GET'], + url: endpoint.url, + version: endpoint.version, + updateParams: endpoint.updateParams || false + } } module.exports = function (routerOpts = {}, routerFactory = require('find-my-way')) { - routerOpts.defaultRoute = handlers.default - function exec (options, isIff = true) { const middleware = this + let router = null + let customFn = null + + // Process options efficiently + if (typeof options === 'function') { + customFn = options + } else { + const endpoints = Array.isArray(options) ? options : options?.endpoints + + if (endpoints?.length) { + // Try to get cached router first + let cache = routerCache.get(routerOpts) + if (!cache) { + cache = new Map() + routerCache.set(routerOpts, cache) + } + + const cacheKey = JSON.stringify(endpoints) + router = cache.get(cacheKey) - // independent router instance per config - const router = routerFactory(routerOpts) - - const opts = typeof options === 'function' ? { custom: options } : (Array.isArray(options) ? { endpoints: options } : options) - if (opts.endpoints && opts.endpoints.length) { - // setup matching router - opts.endpoints - .map(endpoint => typeof endpoint === 'string' ? { url: endpoint } : endpoint) - .forEach(({ methods = ['GET'], url, version, updateParams = false }) => { - if (version) { - router.on(methods, url, { constraints: { version } }, handlers.match(updateParams)) - } else { - router.on(methods, url, handlers.match(updateParams)) + if (!router) { + router = routerFactory({ ...routerOpts, defaultRoute: defaultHandler }) + + // Normalize and register routes + const normalized = endpoints.map(normalizeEndpoint) + for (const { methods, url, version, updateParams } of normalized) { + const handler = createMatchHandler(updateParams) + + if (version) { + router.on(methods, url, { constraints: { version } }, handler) + } else { + router.on(methods, url, handler) + } } - }) + + cache.set(cacheKey, router) + } + } + + if (options?.custom) { + customFn = options.custom + } } + // Optimized execution function const result = function (req, res, next) { - // supporting custom matching function - if (opts.custom) { - if (opts.custom(req)) { - if (isIff) { - return middleware(req, res, next) - } - } else if (!isIff) { - return middleware(req, res, next) - } + let shouldExecute = false - // leave here and do not process opts.endpoints - return next() + if (customFn) { + shouldExecute = customFn(req) + } else if (router) { + shouldExecute = router.lookup(req, res) } - // matching endpoints and moving forward - if (router.lookup(req, res)) { - if (isIff) { - return middleware(req, res, next) - } - } else if (!isIff) { + // Simplified logic: execute middleware if conditions match + if ((isIff && shouldExecute) || (!isIff && !shouldExecute)) { return middleware(req, res, next) } return next() } - // allowing chaining + // Allow chaining result.iff = iff result.unless = unless diff --git a/package.json b/package.json index c041a23..339bfad 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "if", "unless" ], - "files": [ - "README.md" - ], "engines": { "node": ">=8" }, + "files": [ + "index.js", + "index.d.ts", + "README.md" + ], + "typings": "index.d.ts", "author": "Rolando Santamaria Maso ", "license": "MIT", "bugs": { @@ -29,13 +32,13 @@ "homepage": "https://github.com/jkyberneees/middleware-if-unless#readme", "devDependencies": { "chai": "^4.3.7", - "express-unless": "^1.0.0", - "mocha": "^10.2.0", - "nyc": "^15.1.0", - "restana": "^4.9.7", - "supertest": "^6.3.3" + "express-unless": "^2.1.3", + "mocha": "^11.7.2", + "nyc": "^17.1.0", + "restana": "^5.1.0", + "supertest": "^7.1.4" }, "dependencies": { - "find-my-way": "^9.0.1" + "find-my-way": "^9.3.0" } }