From 8c9e8dcb0c7268af7e0a5dedccc5fd7456c206a4 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Mon, 8 Apr 2019 11:42:37 -0700 Subject: [PATCH] feat(rest): apply global interceptors for handler routes --- .../caching-interceptor.acceptance.ts | 102 +++------------ .../caching-interceptor.ts | 100 ++++++++++++++ .../global-caching-interceptor.acceptance.ts | 122 ++++++++++++++++++ packages/rest/src/router/handler-route.ts | 11 +- 4 files changed, 248 insertions(+), 87 deletions(-) create mode 100644 packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.ts create mode 100644 packages/rest/src/__tests__/acceptance/caching-interceptor/global-caching-interceptor.acceptance.ts diff --git a/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.acceptance.ts b/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.acceptance.ts index 9de5be8f2d19..4a5a7cd955fd 100644 --- a/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.acceptance.ts +++ b/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.acceptance.ts @@ -3,14 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import { - inject, - intercept, - Interceptor, - InvocationContext, - Provider, - ValueOrPromise, -} from '@loopback/context'; +import {intercept} from '@loopback/context'; import {get, param} from '@loopback/openapi-v3'; import { Client, @@ -18,13 +11,18 @@ import { expect, givenHttpServerConfig, } from '@loopback/testlab'; -import {Request, RestApplication, RestBindings} from '../../..'; +import {RestApplication} from '../../..'; +import { + cache, + cachedResults, + CachingInterceptorProvider, + clearCache, + status, +} from './caching-interceptor'; describe('caching interceptor', () => { let client: Client; let app: RestApplication; - let returnUpperCaseFromCache = false; - let returnLowerCaseFromCache = false; before(givenAClient); after(async () => { @@ -34,44 +32,45 @@ describe('caching interceptor', () => { context('toUpperCase with bound caching interceptor', () => { it('invokes the controller method if not cached', async () => { await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); - expect(returnUpperCaseFromCache).to.be.false(); + expect(status.returnFromCache).to.be.false(); }); it('returns from cache without invoking the controller method', async () => { for (let i = 0; i <= 5; i++) { await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); - expect(returnUpperCaseFromCache).to.be.true(); + expect(status.returnFromCache).to.be.true(); } }); it('invokes the controller method after cache is cleared', async () => { - CachingInterceptorProvider.clearCache(); + clearCache(); await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); - expect(returnUpperCaseFromCache).to.be.false(); + expect(status.returnFromCache).to.be.false(); }); }); context('toLowerCase with cache interceptor function', () => { it('invokes the controller method if not cached', async () => { await client.get('/toLowerCase/Hello').expect(200, 'hello'); - expect(returnLowerCaseFromCache).to.be.false(); + expect(status.returnFromCache).to.be.false(); }); it('returns from cache without invoking the controller method', async () => { for (let i = 0; i <= 5; i++) { await client.get('/toLowerCase/Hello').expect(200, 'hello'); - expect(returnLowerCaseFromCache).to.be.true(); + expect(status.returnFromCache).to.be.true(); } }); it('invokes the controller method after cache is cleared', async () => { cachedResults.clear(); await client.get('/toLowerCase/Hello').expect(200, 'hello'); - expect(returnLowerCaseFromCache).to.be.false(); + expect(status.returnFromCache).to.be.false(); }); }); async function givenAClient() { + clearCache(); app = new RestApplication({rest: givenHttpServerConfig()}); app.bind('caching-interceptor').toProvider(CachingInterceptorProvider); app.controller(StringCaseController); @@ -95,71 +94,4 @@ describe('caching interceptor', () => { return text.toLowerCase(); } } - - /** - * A provider class for caching interceptor that leverages dependency - * injection - */ - class CachingInterceptorProvider implements Provider { - private static cache = new Map(); - - static clearCache() { - this.cache.clear(); - } - - constructor( - @inject(RestBindings.Http.REQUEST, {optional: true}) - private request: Request | undefined, - ) {} - value() { - return async ( - invocationCtx: InvocationContext, - next: () => ValueOrPromise, - ) => { - returnUpperCaseFromCache = false; - - if (!this.request) { - // The method is not invoked by an http request, no caching - return await next(); - } - const url = this.request.url; - const cachedValue = CachingInterceptorProvider.cache.get(url); - if (cachedValue) { - returnUpperCaseFromCache = true; - return cachedValue as T; - } - const result = await next(); - CachingInterceptorProvider.cache.set(url, result); - return result; - }; - } - } - - /** - * An interceptor function that caches results. It uses `invocationContext` - * to locate the http request - */ - const cachedResults = new Map(); - async function cache( - invocationCtx: InvocationContext, - next: () => ValueOrPromise, - ) { - returnLowerCaseFromCache = false; - const req = await invocationCtx.get(RestBindings.Http.REQUEST, { - optional: true, - }); - if (!req) { - // The method is not invoked by an http request, no caching - return await next(); - } - const url = req.url; - const cachedValue = cachedResults.get(url); - if (cachedValue) { - returnLowerCaseFromCache = true; - return cachedValue as T; - } - const result = await next(); - cachedResults.set(url, result); - return result; - } }); diff --git a/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.ts b/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.ts new file mode 100644 index 000000000000..5999be1a1628 --- /dev/null +++ b/packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.ts @@ -0,0 +1,100 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + inject, + Interceptor, + InvocationContext, + Provider, + ValueOrPromise, +} from '@loopback/context'; +import {Request, RestBindings} from '../../..'; + +/** + * Execution status + */ +export const status = { + returnFromCache: false, +}; + +/** + * An interceptor function that caches results. It uses `invocationContext` + * to locate the http request + */ +export const cachedResults = new Map(); + +/** + * Reset the cache + */ +export function clearCache() { + status.returnFromCache = false; + cachedResults.clear(); +} + +/** + * A provider class for caching interceptor that leverages dependency + * injection + */ +export class CachingInterceptorProvider implements Provider { + constructor( + @inject(RestBindings.Http.REQUEST, {optional: true}) + private request: Request | undefined, + ) {} + value() { + return ( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) => this.intercept(invocationCtx, next); + } + + async intercept( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, + ) { + status.returnFromCache = false; + + if (!this.request) { + // The method is not invoked by an http request, no caching + return await next(); + } + const url = this.request.url; + const cachedValue = cachedResults.get(url); + if (cachedValue) { + status.returnFromCache = true; + return cachedValue as T; + } + const result = await next(); + cachedResults.set(url, result); + return result; + } +} + +/** + * An interceptor function for caching + * @param invocationCtx + * @param next + */ +export async function cache( + invocationCtx: InvocationContext, + next: () => ValueOrPromise, +) { + status.returnFromCache = false; + const req = await invocationCtx.get(RestBindings.Http.REQUEST, { + optional: true, + }); + if (!req || req.method.toLowerCase() !== 'get') { + // The method is not invoked by an http request, no caching + return await next(); + } + const url = req.url; + const cachedValue = cachedResults.get(url); + if (cachedValue) { + status.returnFromCache = true; + return cachedValue as T; + } + const result = await next(); + cachedResults.set(url, result); + return result; +} diff --git a/packages/rest/src/__tests__/acceptance/caching-interceptor/global-caching-interceptor.acceptance.ts b/packages/rest/src/__tests__/acceptance/caching-interceptor/global-caching-interceptor.acceptance.ts new file mode 100644 index 000000000000..d3abe773cee9 --- /dev/null +++ b/packages/rest/src/__tests__/acceptance/caching-interceptor/global-caching-interceptor.acceptance.ts @@ -0,0 +1,122 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/rest +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {asInterceptor} from '@loopback/context'; +import {anOperationSpec} from '@loopback/openapi-spec-builder'; +import {get, param} from '@loopback/openapi-v3'; +import { + Client, + createRestAppClient, + expect, + givenHttpServerConfig, +} from '@loopback/testlab'; +import {RestApplication} from '../../..'; +import { + cachedResults, + CachingInterceptorProvider, + clearCache, + status, +} from './caching-interceptor'; + +describe('global caching interceptor', () => { + let client: Client; + let app: RestApplication; + + before(givenAClient); + after(async () => { + await app.stop(); + }); + + context('toUpperCase', () => { + it('invokes the controller method if not cached', async () => { + await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); + expect(status.returnFromCache).to.be.false(); + }); + + it('returns from cache without invoking the controller method', async () => { + for (let i = 0; i <= 5; i++) { + await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); + expect(status.returnFromCache).to.be.true(); + } + }); + + it('invokes the controller method after cache is cleared', async () => { + clearCache(); + await client.get('/toUpperCase/Hello').expect(200, 'HELLO'); + expect(status.returnFromCache).to.be.false(); + }); + }); + + context('toLowerCase', () => { + it('invokes the handler function if not cached', async () => { + await client.get('/toLowerCase/Hello').expect(200, 'hello'); + expect(status.returnFromCache).to.be.false(); + }); + + it('returns from cache without invoking the handler function', async () => { + for (let i = 0; i <= 5; i++) { + await client.get('/toLowerCase/Hello').expect(200, 'hello'); + expect(status.returnFromCache).to.be.true(); + } + }); + + it('invokes the handler function after cache is cleared', async () => { + cachedResults.clear(); + await client.get('/toLowerCase/Hello').expect(200, 'hello'); + expect(status.returnFromCache).to.be.false(); + }); + }); + + /** + * OpenAPI operation spec for `toLowerCase(text: string)` + */ + const toLowerCaseOperationSpec = anOperationSpec() + .withOperationName('toLowerCase') + .withParameter({ + name: 'text', + in: 'path', + schema: { + type: 'string', + }, + }) + .withStringResponse() + .build(); + + /** + * A plain function to convert `text` to lower case + * @param text + */ + function toLowerCase(text: string) { + return text.toLowerCase(); + } + + async function givenAClient() { + clearCache(); + app = new RestApplication({rest: givenHttpServerConfig()}); + app + .bind('caching-interceptor') + .toProvider(CachingInterceptorProvider) + .apply(asInterceptor); + app.controller(StringCaseController); + app.route( + 'get', + '/toLowerCase/{text}', + toLowerCaseOperationSpec, + toLowerCase, + ); + await app.start(); + client = createRestAppClient(app); + } + + /** + * A controller using interceptors for caching + */ + class StringCaseController { + @get('/toUpperCase/{text}') + toUpperCase(@param.path.string('text') text: string) { + return text.toUpperCase(); + } + } +}); diff --git a/packages/rest/src/router/handler-route.ts b/packages/rest/src/router/handler-route.ts index 999f443fd632..5d972fa16823 100644 --- a/packages/rest/src/router/handler-route.ts +++ b/packages/rest/src/router/handler-route.ts @@ -3,7 +3,7 @@ // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT -import {Context} from '@loopback/context'; +import {Context, invokeMethodWithInterceptors} from '@loopback/context'; import {OperationObject} from '@loopback/openapi-v3-types'; import {OperationArgs, OperationRetval} from '../types'; import {BaseRoute} from './base-route'; @@ -30,6 +30,13 @@ export class Route extends BaseRoute { requestContext: Context, args: OperationArgs, ): Promise { - return await this._handler(...args); + // Use `invokeMethodWithInterceptors` to invoke the handler function so + // that global interceptors are applied + return await invokeMethodWithInterceptors( + requestContext, + this, + '_handler', + args, + ); } }