diff --git a/express/README.MD b/express/README.MD index 828d4b9..f66d4ce 100644 --- a/express/README.MD +++ b/express/README.MD @@ -687,13 +687,17 @@ class ThingRouter { } ``` -A final option marks your custom decorator's middlewares for **deduplication**: +You can mark your custom decorator's middlewares for **deduplication**: ```ts -const CurrentUser = createParamDecorator((req) => req.user, [isAuthenticated], true) +const CurrentUser = createParamDecorator( + (req) => req.user, + [{ handler: isAuthenticated, dedupeByReference: true, dedupeByName: true }] +) ``` -With this option turned on, on registering, Reflet won't add the implicit middlewares if they're already applied locally (on a route or router) or globally (on the app). _Comparison is done by **reference** and by **name**._ +With these options, on registering, Reflet won't add the implicit middlewares if they're already applied locally (on a route or router) or globally (on the app). +Comparison is done by function reference with `dedupeByReference` and by function name with `dedupeByName`. That's basically how the `Body` decorator works with its body parsers. @@ -707,8 +711,10 @@ const BodyTrimmed = (key: string) => createParamDecorator( if (typeof req.body[key] === 'string') return req.body[key].trim() else return req.body[key] }, - [express.json(), express.urlencoded()], - true + [ + { handler: express.json(), dedupeByReference: true, dedupeByName: true }, + { handler: express.urlencoded(), dedupeByReference: true, dedupeByName: true }, + ] ) @Router('/things') diff --git a/express/__tests__/param.test.ts b/express/__tests__/param.test.ts index 48a7a9c..43e863a 100644 --- a/express/__tests__/param.test.ts +++ b/express/__tests__/param.test.ts @@ -135,7 +135,7 @@ describe('param middlewares deduplication', () => { test('body-parsers in different param decorators', async () => { const BodyTrimmed = (subKey: string) => - createParamDecorator((req) => req.body[subKey].trim(), [express.json()], true) + createParamDecorator((req) => req.body[subKey].trim(), [{ handler: express.json(), dedupeByName: true }]) class Foo { @Post() @@ -161,7 +161,10 @@ describe('param middlewares deduplication', () => { next() } - const CurrentUser = createParamDecorator((req: RequestAuth) => req.user!, [authent], true) + const CurrentUser = createParamDecorator( + (req: RequestAuth) => req.user!, + [{ handler: authent, dedupeByReference: true }] + ) @Use(authent) class Bar { diff --git a/express/src/param-decorators.ts b/express/src/param-decorators.ts index 38723a5..5cff893 100644 --- a/express/src/param-decorators.ts +++ b/express/src/param-decorators.ts @@ -5,15 +5,6 @@ import { RefletExpressError } from './reflet-error' const META = Symbol('param') -/** - * @internal - */ -type ParamMeta = { - readonly mapper: (req: express.Request, res: express.Response, next?: express.NextFunction) => any - readonly use?: express.RequestHandler[] - readonly dedupeUse?: boolean -} - /** * Injects Request object in the method's parameters. * @see https://expressjs.com/en/4x/api.html#req @@ -125,7 +116,10 @@ export namespace Next { } /** default parser middlewares to apply with @Body decorator */ -const bodyParsers = [express.json(), express.urlencoded({ extended: true })] +const bodyParsers: createParamDecorator.Middleware[] = [ + { handler: express.json(), dedupeByReference: true, dedupeByName: true }, + { handler: express.urlencoded({ extended: true }), dedupeByReference: true, dedupeByName: true }, +] /** * Injects request body in the method's parameters. @@ -160,10 +154,10 @@ export function Body<T extends object>( ) { if (arguments.length === 3 && typeof keyOrTarget === 'object') { const target = keyOrTarget - return createParamDecorator((req) => req.body, bodyParsers, true)(target, propertyKey!, parameterIndex!) + return createParamDecorator((req) => req.body, bodyParsers)(target, propertyKey!, parameterIndex!) } else { const subKey = keyOrTarget as keyof T | undefined - return createParamDecorator((req) => (subKey ? req.body[subKey] : req.body), bodyParsers, true) + return createParamDecorator((req) => (subKey ? req.body[subKey] : req.body), bodyParsers) } } @@ -310,8 +304,7 @@ export namespace Headers { * Creates a parameter decorator, to inject anything we want in decorated routes. * * @param mapper - function that should return the thing we want to inject from the Request object. - * @param use - adds middlewares to the route if the mapper needs them (_e.g. we need body-parser middlewares to retrieve `req.body`_). - * @param dedupeUse - marks the middlewares for deduplication based on the function reference and name (_e.g. if 'jsonParser' is already in use locally or globally, it won't be added again_). + * @param use - adds middlewares to the route if the mapper needs them (_e.g. we need body-parser middlewares to retrieve `req.body`_). You can pass options to deduplicate middlewares based on the handler function reference or name (_e.g. if 'jsonParser' is already in use locally or globally, it won't be added again_). * * @remarks * We can create decorators with or without options. @@ -333,8 +326,7 @@ export namespace Headers { * // Advanced decorator (with option and middleware): * const BodyTrimmed = (key: string) => createParamDecorator( * (req) => req.body[key].trim(), - * [express.json()], - * true + * [{ handler: express.json(), dedupeByReference: true, dedupeByName: true }] * ) * class Foo { * @Post('/message') @@ -346,11 +338,10 @@ export namespace Headers { */ export function createParamDecorator<T = any>( mapper: (req: express.Request, res: express.Response) => T, - use?: express.RequestHandler[], - dedupeUse?: boolean + use?: createParamDecorator.Middleware[] ): createParamDecorator.Decorator { return (target, key, index) => { - const params: ParamMeta[] = Reflect.getOwnMetadata(META, target, key) || [] + const params: createParamDecorator.Options[] = Reflect.getOwnMetadata(META, target, key) || [] if (params[index]) { const codePath = `${target.constructor.name}.${key.toString()}` @@ -361,12 +352,27 @@ export function createParamDecorator<T = any>( ) } - params[index] = { mapper, use, dedupeUse } + params[index] = { mapper, use } Reflect.defineMetadata(META, params, target, key) } } export namespace createParamDecorator { + /** + * @public + */ + export type Options = { + readonly mapper: (req: express.Request, res: express.Response, next?: express.NextFunction) => any + readonly use?: Middleware[] + } + + /** + * @public + */ + export type Middleware = + | express.RequestHandler + | { handler: express.RequestHandler; dedupeByReference?: boolean; dedupeByName?: boolean } + /** * Equivalent to `ParameterDecorator`. * @public @@ -385,7 +391,7 @@ export function extractParams( key: string | symbol, { req, res, next }: { req: express.Request; res: express.Response; next: express.NextFunction } ): any[] { - const params: ParamMeta[] = Reflect.getOwnMetadata(META, target.prototype, key) || [] + const params: createParamDecorator.Options[] = Reflect.getOwnMetadata(META, target.prototype, key) || [] // No decorator found in the method: simply return the original arguments in the original order if (!params.length) return [req, res, next] @@ -417,37 +423,56 @@ export function extractParamsMiddlewares( key: string | symbol, alreadyMwares: express.RequestHandler[][] ): express.RequestHandler[] { - const params: ParamMeta[] = Reflect.getOwnMetadata(META, target.prototype, key) || [] + const params: createParamDecorator.Options[] = Reflect.getOwnMetadata(META, target.prototype, key) || [] if (!params.length) return [] const paramMwares: express.RequestHandler[] = [] let alreadyNames: string[] = [] - for (const { use, dedupeUse } of params) { + for (const { use } of params) { if (!use) continue - if (dedupeUse && !alreadyNames.length) { - // Perform the flatmap only if one of the parameter requires deduplication. - alreadyNames = flatMapFast(alreadyMwares, (m) => m.name) - } - for (const mware of use) { - // If the same param decorator is used twice, we prevent adding its middlewares twice - // whether or not it's marked for dedupe, to do that we simply compare by reference. - if (paramMwares.includes(mware)) continue - - // Dedupe middlewares in upper layers. - if (dedupeUse) { - const sameRef = alreadyMwares.some((alreadyMware) => alreadyMware.includes(mware)) - const sameName = !!mware.name && alreadyNames.includes(mware.name) - - // console.log('dedupe:', mware.name, sameRef, sameName) - if (sameRef || sameName) continue + if (typeof mware === 'function') { + // If the same param decorator is used twice, we prevent adding its middlewares twice + // whether or not it's marked for dedupe, to do that we simply compare by reference. + if (paramMwares.includes(mware)) { + continue + } + + paramMwares.push(mware) + } else { + if (paramMwares.includes(mware.handler)) { + continue + } + + // Dedupe middlewares in upper layers. + + if (mware.dedupeByReference) { + const sameRef = alreadyMwares.some((alreadyMware) => alreadyMware.includes(mware.handler)) + // console.log('dedupe-by-reference:', mware.handler.name, sameRef) + if (sameRef) { + continue + } + } + + if (mware.dedupeByName) { + // Perform the flatmap only if one of the parameter requires deduplication by name. + if (!alreadyNames.length) { + alreadyNames = flatMapFast(alreadyMwares, (m) => m.name) + } + + const sameName = !!mware.handler.name && alreadyNames.includes(mware.handler.name) + // console.log('dedupe-by-name:', mware.handler.name, sameName) + if (sameName) { + continue + } + } + + paramMwares.push(mware.handler) } - paramMwares.push(mware) - // todo?: alreadyNames.push(mware.name) // Should we dedupe by *name* the same middleware on different param decorators ? // Or should we just warn the user of a potential conflict, and suggest to move