From 18be9cb1af2e0148522dd8210185aa32db366828 Mon Sep 17 00:00:00 2001 From: "romain.lenzotti" Date: Thu, 13 Jun 2019 19:27:20 +0200 Subject: [PATCH] feat(common): Add UseBeforeEach decorator. --- .../interceptors/interceptor-example.ts | 8 +- .../interceptors/interceptor-usage.ts | 3 +- packages/common/src/mvc/decorators/index.ts | 1 + .../src/mvc/decorators/method/useAfter.ts | 20 ++-- .../src/mvc/decorators/method/useBefore.ts | 22 ++-- .../mvc/decorators/method/useBeforeEach.ts | 38 +++++++ .../mvc/decorators/method/useAfter.spec.ts | 93 ++++++++++++---- .../mvc/decorators/method/useBefore.spec.ts | 95 ++++++++++++---- .../decorators/method/useBeforeEach.spec.ts | 101 ++++++++++++++++++ packages/di/src/interfaces/IInterceptor.ts | 9 +- .../di/src/registries/ProviderRegistry.ts | 5 +- packages/di/src/services/InjectorService.ts | 38 +++++-- packages/di/test/decorators/intercept.spec.ts | 2 +- .../di/test/integration/interceptor.spec.ts | 8 +- .../di/test/services/InjectorService.spec.ts | 35 +++++- 15 files changed, 391 insertions(+), 87 deletions(-) create mode 100644 packages/common/src/mvc/decorators/method/useBeforeEach.ts create mode 100644 packages/common/test/mvc/decorators/method/useBeforeEach.spec.ts diff --git a/docs/docs/snippets/interceptors/interceptor-example.ts b/docs/docs/snippets/interceptors/interceptor-example.ts index 8f23f67bcef..2928bd0c043 100644 --- a/docs/docs/snippets/interceptors/interceptor-example.ts +++ b/docs/docs/snippets/interceptors/interceptor-example.ts @@ -8,16 +8,14 @@ export class MyInterceptor implements IInterceptor { * * opts: Static params that can be provided when the interceptor is attached to a specific method */ - intercept(context: IInterceptorContext, next: IInterceptorNextHandler) { + async intercept(context: IInterceptorContext, next: IInterceptorNextHandler) { console.log(`the method ${context.propertyKey} will be executed with args ${context.args} and static data ${context.options}`); - // let the original method proceed - const result = context.next(); + // let the original method by calling next function + const result = await next(); console.log(`the method was executed, and returned ${result}`); // must return the returned value back to the caller - // the retValue might be a promise in which case you can use .then to chain other code logic - // or you can use async aroundInvoke and await ctx.proceed() to execute the code in linear fashion return result; } } diff --git a/docs/docs/snippets/interceptors/interceptor-usage.ts b/docs/docs/snippets/interceptors/interceptor-usage.ts index 622ed5c1c3d..daf2c72a035 100644 --- a/docs/docs/snippets/interceptors/interceptor-usage.ts +++ b/docs/docs/snippets/interceptors/interceptor-usage.ts @@ -3,8 +3,7 @@ import {MyInterceptor} from "../interceptors/MyInterceptor"; export class MyService { // MyInterceptor is going to be used to intercept this method whenever called - // 'simple data' is static data that will be passed as second arg the the interceptor aroundInvoke - // this can be any data, you can pass array or object for that matter + // 'simple data' is static data that will be passed as second arg to the interceptor.intercept method @Intercept(MyInterceptor, "simple data") mySimpleMethod() { console.log("the simple method is executed"); diff --git a/packages/common/src/mvc/decorators/index.ts b/packages/common/src/mvc/decorators/index.ts index 854e43230f7..4c82af9f759 100644 --- a/packages/common/src/mvc/decorators/index.ts +++ b/packages/common/src/mvc/decorators/index.ts @@ -1,6 +1,7 @@ // Method export * from "./method/use"; export * from "./method/useBefore"; +export * from "./method/useBeforeEach"; export * from "./method/useAfter"; export * from "./method/useAuth"; export * from "./method/route"; diff --git a/packages/common/src/mvc/decorators/method/useAfter.ts b/packages/common/src/mvc/decorators/method/useAfter.ts index 12753533ff0..44252e17575 100644 --- a/packages/common/src/mvc/decorators/method/useAfter.ts +++ b/packages/common/src/mvc/decorators/method/useAfter.ts @@ -1,4 +1,4 @@ -import {getDecoratorType, Store, Type} from "@tsed/core"; +import {DecoratorParameters, getDecoratorType, StoreMerge, UnsupportedDecoratorType} from "@tsed/core"; import {EndpointRegistry} from "../../registries/EndpointRegistry"; /** @@ -22,15 +22,17 @@ import {EndpointRegistry} from "../../registries/EndpointRegistry"; * @endpoint */ export function UseAfter(...args: any[]): Function { - return (target: Type, targetKey?: string, descriptor?: TypedPropertyDescriptor): TypedPropertyDescriptor | void => { - if (getDecoratorType([target, targetKey, descriptor]) === "method") { - EndpointRegistry.useAfter(target, targetKey!, args); + return (...decoratorArgs: DecoratorParameters): TypedPropertyDescriptor | void => { + switch (getDecoratorType(decoratorArgs, true)) { + case "method": + EndpointRegistry.useAfter(decoratorArgs[0], decoratorArgs[1]!, args); - return descriptor; + return decoratorArgs[2] as any; + case "class": + StoreMerge("middlewares", {useAfter: args})(...decoratorArgs); + break; + default: + throw new UnsupportedDecoratorType(UseAfter, decoratorArgs); } - - Store.from(target).merge("middlewares", { - useAfter: args - }); }; } diff --git a/packages/common/src/mvc/decorators/method/useBefore.ts b/packages/common/src/mvc/decorators/method/useBefore.ts index 20015d3f890..38945165c91 100644 --- a/packages/common/src/mvc/decorators/method/useBefore.ts +++ b/packages/common/src/mvc/decorators/method/useBefore.ts @@ -1,4 +1,4 @@ -import {getDecoratorType, Store, Type} from "@tsed/core"; +import {DecoratorParameters, getDecoratorType, StoreMerge, UnsupportedDecoratorType} from "@tsed/core"; import {EndpointRegistry} from "../../registries/EndpointRegistry"; /** @@ -7,7 +7,7 @@ import {EndpointRegistry} from "../../registries/EndpointRegistry"; * * ```typescript * @Controller('/') - * @UseBefore(Middleware1) + * @UseBefore(Middleware1) // called only one time before all endpoint * export class Ctrl { * * @Get('/') @@ -22,15 +22,17 @@ import {EndpointRegistry} from "../../registries/EndpointRegistry"; * @endpoint */ export function UseBefore(...args: any[]): Function { - return (target: Type, targetKey?: string, descriptor?: TypedPropertyDescriptor): TypedPropertyDescriptor | void => { - if (getDecoratorType([target, targetKey, descriptor]) === "method") { - EndpointRegistry.useBefore(target, targetKey!, args); + return (...decoratorArgs: DecoratorParameters): TypedPropertyDescriptor | void => { + switch (getDecoratorType(decoratorArgs, true)) { + case "method": + EndpointRegistry.useBefore(decoratorArgs[0], decoratorArgs[1]!, args); - return descriptor; + return decoratorArgs[2] as any; + case "class": + StoreMerge("middlewares", {useBefore: args})(...decoratorArgs); + break; + default: + throw new UnsupportedDecoratorType(UseBefore, decoratorArgs); } - - Store.from(target).merge("middlewares", { - useBefore: args - }); }; } diff --git a/packages/common/src/mvc/decorators/method/useBeforeEach.ts b/packages/common/src/mvc/decorators/method/useBeforeEach.ts new file mode 100644 index 00000000000..ed7ee489a09 --- /dev/null +++ b/packages/common/src/mvc/decorators/method/useBeforeEach.ts @@ -0,0 +1,38 @@ +import {decorateMethodsOf, DecoratorParameters, getDecoratorType, UnsupportedDecoratorType} from "@tsed/core"; +import {UseBefore} from "./UseBefore"; + +/** + * Mounts the specified middleware function or functions at the specified path: the middleware function is executed when + * the base of the requested path matches `path. + * + * ```typescript + * @Controller('/') + * @UseBeforeEach(Middleware1) // Called before each endpoint + * export class Ctrl { + * + * @Get('/') + * get() { } + * } + * + * ``` + * + * @returns {Function} + * @param args + * @decorator + * @endpoint + */ +export function UseBeforeEach(...args: any[]): Function { + return (...decoratorArgs: DecoratorParameters): TypedPropertyDescriptor | void => { + switch (getDecoratorType(decoratorArgs, true)) { + case "method": + return UseBefore(...args)(...decoratorArgs); + + case "class": + decorateMethodsOf(decoratorArgs[0], UseBefore(...args)); + break; + + default: + throw new UnsupportedDecoratorType(UseBeforeEach, decoratorArgs); + } + }; +} diff --git a/packages/common/test/mvc/decorators/method/useAfter.spec.ts b/packages/common/test/mvc/decorators/method/useAfter.spec.ts index 05c24ac4d46..e7b2eaf0f60 100644 --- a/packages/common/test/mvc/decorators/method/useAfter.spec.ts +++ b/packages/common/test/mvc/decorators/method/useAfter.spec.ts @@ -1,49 +1,100 @@ -import {decoratorArgs, descriptorOf, Store} from "@tsed/core"; -import {expect} from "chai"; +import {prototypeOf, Store, UnsupportedDecoratorType} from "@tsed/core"; import * as Sinon from "sinon"; import {EndpointRegistry, UseAfter} from "../../../../src/mvc"; -class Test { - test() { +class CustomMiddleware { + use() { } } describe("UseAfter()", () => { - describe("when the decorator is use on a method", () => { - before(() => { - this.endpointRegistryStub = Sinon.stub(EndpointRegistry, "useAfter"); + describe("when the decorator is use on a class", () => { + class Test { + test() { + } + } - this.returns = UseAfter(() => { - })(...decoratorArgs(Test, "test")); + before(() => { + Sinon.stub(EndpointRegistry, "useAfter"); }); after(() => { - this.endpointRegistryStub.restore(); + // @ts-ignore + EndpointRegistry.useAfter.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useAfter.resetHistory(); }); it("should add the middleware on the use stack", () => { - this.endpointRegistryStub.should.be.calledWithExactly(Test, "test", [Sinon.match.func]); + // WHEN + UseAfter(CustomMiddleware)(Test); + + // THEN + Store.from(Test).get("middlewares").should.deep.eq({useAfter: [CustomMiddleware]}); + }); + }); + describe("when the decorator is use on a method", () => { + before(() => { + Sinon.stub(EndpointRegistry, "useAfter"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useAfter.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useAfter.resetHistory(); }); - it("should return a descriptor", () => { - this.returns.should.be.deep.eq(descriptorOf(Test, "test")); + it("should add the middleware on the use stack", () => { + // WHEN + class Test { + @UseAfter(CustomMiddleware) + test() { + } + } + + // THEN + EndpointRegistry.useAfter.should.be.calledWithExactly(prototypeOf(Test), "test", [CustomMiddleware]); }); }); + describe("when the decorator is use in another way", () => { + class Test { + test() { + } + } - describe("when the decorator is use on a class", () => { before(() => { - this.returns = UseAfter(() => { - })(Test); + Sinon.stub(EndpointRegistry, "useAfter"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useAfter.restore(); + }); - this.store = Store.from(Test).get("middlewares"); + afterEach(() => { + // @ts-ignore + EndpointRegistry.useAfter.resetHistory(); }); it("should add the middleware on the use stack", () => { - expect(this.store.useAfter[0]).to.be.a("function"); - }); + // WHEN + let actualError; + try { + UseAfter(CustomMiddleware)(Test, "property"); + } catch (er) { + actualError = er; + } - it("should return nothing", () => { - expect(this.returns).to.eq(undefined); + // THEN + actualError.should.instanceOf(UnsupportedDecoratorType); + actualError.message.should.eq("UseAfter cannot used as property.static at Test.property"); }); }); }); diff --git a/packages/common/test/mvc/decorators/method/useBefore.spec.ts b/packages/common/test/mvc/decorators/method/useBefore.spec.ts index 95baa6b600a..f109202076c 100644 --- a/packages/common/test/mvc/decorators/method/useBefore.spec.ts +++ b/packages/common/test/mvc/decorators/method/useBefore.spec.ts @@ -1,47 +1,100 @@ -import {decoratorArgs, descriptorOf, Store} from "@tsed/core"; -import {expect} from "chai"; +import {prototypeOf, Store, UnsupportedDecoratorType} from "@tsed/core"; import * as Sinon from "sinon"; import {EndpointRegistry, UseBefore} from "../../../../src/mvc"; -class Test { - test() { +class CustomMiddleware { + use() { } } describe("UseBefore()", () => { - describe("when the decorator is use on a method", () => { - before(() => { - this.endpointRegistryStub = Sinon.stub(EndpointRegistry, "useBefore"); + describe("when the decorator is use on a class", () => { + class Test { + test() { + } + } - this.returns = UseBefore(() => { - })(...decoratorArgs(Test, "test")); + before(() => { + Sinon.stub(EndpointRegistry, "useBefore"); }); after(() => { - this.endpointRegistryStub.restore(); + // @ts-ignore + EndpointRegistry.useBefore.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); }); it("should add the middleware on the use stack", () => { - this.endpointRegistryStub.should.be.calledWithExactly(Test, "test", [Sinon.match.func]); + // WHEN + UseBefore(CustomMiddleware)(Test); + + // THEN + Store.from(Test).get("middlewares").should.deep.eq({useBefore: [CustomMiddleware]}); + }); + }); + describe("when the decorator is use on a method", () => { + before(() => { + Sinon.stub(EndpointRegistry, "useBefore"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useBefore.restore(); }); - it("should return a descriptor", () => { - this.returns.should.be.deep.eq(descriptorOf(Test, "test")); + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); + }); + + it("should add the middleware on the use stack", () => { + // WHEN + class Test { + @UseBefore(CustomMiddleware) + test() { + } + } + + // THEN + EndpointRegistry.useBefore.should.be.calledWithExactly(prototypeOf(Test), "test", [CustomMiddleware]); }); }); + describe("when the decorator is use in another way", () => { + class Test { + test() { + } + } - describe("when the decorator is use on a class", () => { before(() => { - this.returns = UseBefore(() => { - })(Test); + Sinon.stub(EndpointRegistry, "useBefore"); + }); - this.store = Store.from(Test).get("middlewares"); + after(() => { + // @ts-ignore + EndpointRegistry.useBefore.restore(); }); - it("should add the middleware on the use stack", () => { - expect(this.store.useBefore[0]).to.be.a("function"); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); }); - it("should return nothing", () => { - expect(this.returns).to.eq(undefined); + + it("should add the middleware on the use stack", () => { + // WHEN + let actualError; + try { + UseBefore(CustomMiddleware)(Test, "property"); + } catch (er) { + actualError = er; + } + + // THEN + actualError.should.instanceOf(UnsupportedDecoratorType); + actualError.message.should.eq("UseBefore cannot used as property.static at Test.property"); }); }); }); diff --git a/packages/common/test/mvc/decorators/method/useBeforeEach.spec.ts b/packages/common/test/mvc/decorators/method/useBeforeEach.spec.ts new file mode 100644 index 00000000000..6c4a8c46057 --- /dev/null +++ b/packages/common/test/mvc/decorators/method/useBeforeEach.spec.ts @@ -0,0 +1,101 @@ +import {prototypeOf, UnsupportedDecoratorType} from "@tsed/core"; +import * as Sinon from "sinon"; +import {EndpointRegistry, UseBeforeEach} from "../../../../src/mvc"; + +class CustomMiddleware { + use() { + } +} + +describe("UseBeforeEach()", () => { + describe("when the decorator is use on a class", () => { + class Test { + test() { + } + } + + before(() => { + Sinon.stub(EndpointRegistry, "useBefore"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useBefore.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); + }); + + it("should add the middleware on the use stack", () => { + // WHEN + UseBeforeEach(CustomMiddleware)(Test); + + // THEN + EndpointRegistry.useBefore.should.be.calledWithExactly(prototypeOf(Test), "test", [CustomMiddleware]); + }); + }); + describe("when the decorator is use on a method", () => { + before(() => { + Sinon.stub(EndpointRegistry, "useBefore"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useBefore.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); + }); + + it("should add the middleware on the use stack", () => { + // WHEN + class Test { + @UseBeforeEach(CustomMiddleware) + test() { + } + } + + // THEN + EndpointRegistry.useBefore.should.be.calledWithExactly(prototypeOf(Test), "test", [CustomMiddleware]); + }); + }); + + describe("when the decorator is use in another way", () => { + class Test { + test() { + } + } + + before(() => { + Sinon.stub(EndpointRegistry, "useBefore"); + }); + + after(() => { + // @ts-ignore + EndpointRegistry.useBefore.restore(); + }); + + afterEach(() => { + // @ts-ignore + EndpointRegistry.useBefore.resetHistory(); + }); + + it("should add the middleware on the use stack", () => { + // WHEN + let actualError; + try { + UseBeforeEach(CustomMiddleware)(Test, "property"); + } catch (er) { + actualError = er; + } + + // THEN + actualError.should.instanceOf(UnsupportedDecoratorType); + actualError.message.should.eq("UseBeforeEach cannot used as property.static at Test.property"); + }); + }); +}); diff --git a/packages/di/src/interfaces/IInterceptor.ts b/packages/di/src/interfaces/IInterceptor.ts index 8ca7fe5ab3a..d7acb37abf2 100644 --- a/packages/di/src/interfaces/IInterceptor.ts +++ b/packages/di/src/interfaces/IInterceptor.ts @@ -1,5 +1,10 @@ -import {IInterceptorContext} from "./IInterceptorContext"; +import {IInterceptorContext, IInterceptorNextHandler} from "./IInterceptorContext"; export interface IInterceptor { - aroundInvoke: (ctx: IInterceptorContext, options?: any) => any; + /** + * @deprecated Use intercept instead. + */ + aroundInvoke?(context: IInterceptorContext, options?: any): any; + + intercept?(context: IInterceptorContext, next?: IInterceptorNextHandler): any; } diff --git a/packages/di/src/registries/ProviderRegistry.ts b/packages/di/src/registries/ProviderRegistry.ts index 0a2aabc03c6..e5004411793 100644 --- a/packages/di/src/registries/ProviderRegistry.ts +++ b/packages/di/src/registries/ProviderRegistry.ts @@ -232,8 +232,7 @@ export const registerController = GlobalProviders.createRegisterFn(ProviderType. * import {registerInterceptor, InjectorService} from "@tsed/common"; * * export default class MyInterceptor { - * constructor(){} - * aroundInvoke() { + * intercept() { * return "test"; * } * } @@ -246,7 +245,7 @@ export const registerController = GlobalProviders.createRegisterFn(ProviderType. * injector.load(); * * const myInterceptor = injector.get(MyInterceptor); - * myInterceptor.aroundInvoke(); // test + * myInterceptor.intercept(); // test * ``` * * @param provider Provider configuration. diff --git a/packages/di/src/services/InjectorService.ts b/packages/di/src/services/InjectorService.ts index b0dd4ddc447..e8ef7af35c1 100644 --- a/packages/di/src/services/InjectorService.ts +++ b/packages/di/src/services/InjectorService.ts @@ -1,4 +1,5 @@ import {deepClone, getClass, getClassOrSymbol, isFunction, Metadata, nameOf, prototypeOf, Store} from "@tsed/core"; +import * as util from "util"; import {Container} from "../class/Container"; import {LocalsContainer} from "../class/LocalsContainer"; import {Provider} from "../class/Provider"; @@ -341,21 +342,42 @@ export class InjectorService extends Container { const originalMethod = instance[propertyKey]; instance[propertyKey] = (...args: any[]) => { + const next = (err?: Error) => { + if (!err) { + return originalMethod.apply(instance, args); + } + + throw err; + }; + const context: IInterceptorContext = { target, method: propertyKey, propertyKey, args, - proceed(err?: Error) { - if (!err) { - return originalMethod.apply(instance, args); - } - - throw err; - } + options, + proceed: util.deprecate(next, "context.proceed() is deprecated. Use context.next() or next() parameters instead."), + next }; - return this.get(useType)!.aroundInvoke(context, options); + const interceptor = this.get(useType)!; + + if (interceptor.aroundInvoke) { + interceptor.aroundInvoke = util.deprecate( + interceptor.aroundInvoke.bind(interceptor), + "interceptor.aroundInvoke is deprecated. Use interceptor.intercept instead." + ); + + return interceptor.aroundInvoke!(context, options); + } + + return interceptor.intercept!( + { + ...context, + options + }, + next + ); }; } diff --git a/packages/di/test/decorators/intercept.spec.ts b/packages/di/test/decorators/intercept.spec.ts index 964b2cd51fe..0d0eb6baa56 100644 --- a/packages/di/test/decorators/intercept.spec.ts +++ b/packages/di/test/decorators/intercept.spec.ts @@ -5,7 +5,7 @@ describe("@Intercept", () => { it("should store metadata", () => { // GIVEN class TestInterceptor implements IInterceptor { - aroundInvoke(ctx: IInterceptorContext, options?: any) { + intercept(ctx: IInterceptorContext) { return ""; } diff --git a/packages/di/test/integration/interceptor.spec.ts b/packages/di/test/integration/interceptor.spec.ts index b06e4f1e238..87a828ca47f 100644 --- a/packages/di/test/integration/interceptor.spec.ts +++ b/packages/di/test/integration/interceptor.spec.ts @@ -15,11 +15,11 @@ describe("DI Interceptor", () => { // do some logic } - aroundInvoke(ctx: IInterceptorContext, opts?: string) { - const r = typeof ctx.args[0] === "string" ? undefined : new Error(`Error message`); - const retValue = ctx.proceed(r); + intercept(context: IInterceptorContext) { + const r = typeof context.args[0] === "string" ? undefined : new Error(`Error message`); + const retValue = context.next(r); - return `${retValue} - ${opts || ""} - intercepted`; + return `${retValue} - ${context.options || ""} - intercepted`; } } diff --git a/packages/di/test/services/InjectorService.spec.ts b/packages/di/test/services/InjectorService.spec.ts index 06f90d9d455..696cb88816e 100644 --- a/packages/di/test/services/InjectorService.spec.ts +++ b/packages/di/test/services/InjectorService.spec.ts @@ -568,7 +568,7 @@ describe("InjectorService", () => { after(() => sandbox.restore()); - it("should bind the method", async () => { + it("should bind the method with aroundInvoke", async () => { // GIVEN class InterceptorTest { aroundInvoke(ctx: any) { @@ -599,6 +599,39 @@ describe("InjectorService", () => { expect(originalMethod).should.not.eq(instance.test3); injector.get.should.have.been.calledWithExactly(InterceptorTest); + expect(result).to.eq("test called intercepted"); + }); + it("should bind the method with intercept", async () => { + // GIVEN + class InterceptorTest { + intercept(ctx: any) { + return ctx.next() + " intercepted"; + } + } + + const injector = new InjectorService(); + injector.addProvider(InterceptorTest); + + await injector.load(); + + const instance = new Test(); + const originalMethod = instance["test"]; + + sandbox.spy(injector, "get"); + + // WHEN + injector.bindInterceptor(instance, { + bindingType: "interceptor", + propertyKey: "test3", + useType: InterceptorTest + } as any); + + const result = (instance as any).test3("test"); + + // THEN + expect(originalMethod).should.not.eq(instance.test3); + injector.get.should.have.been.calledWithExactly(InterceptorTest); + expect(result).to.eq("test called intercepted"); }); });