Skip to content
Merged
101 changes: 101 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -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;
114 changes: 72 additions & 42 deletions index.js
Original file line number Diff line number Diff line change
@@ -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

Expand Down
21 changes: 12 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <kyberneees@gmail.com>",
"license": "MIT",
"bugs": {
Expand All @@ -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"
}
}