Skip to content

Commit

Permalink
feat(rest): apply global interceptors for handler routes
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Apr 8, 2019
1 parent d9a2211 commit 8c9e8dc
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 87 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,26 @@
// 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,
createRestAppClient,
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 () => {
Expand All @@ -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);
Expand All @@ -95,71 +94,4 @@ describe('caching interceptor', () => {
return text.toLowerCase();
}
}

/**
* A provider class for caching interceptor that leverages dependency
* injection
*/
class CachingInterceptorProvider implements Provider<Interceptor> {
private static cache = new Map<string, unknown>();

static clearCache() {
this.cache.clear();
}

constructor(
@inject(RestBindings.Http.REQUEST, {optional: true})
private request: Request | undefined,
) {}
value() {
return async <T>(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<T>,
) => {
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<string, unknown>();
async function cache<T>(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<T>,
) {
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;
}
});
Original file line number Diff line number Diff line change
@@ -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<string, unknown>();

/**
* 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<Interceptor> {
constructor(
@inject(RestBindings.Http.REQUEST, {optional: true})
private request: Request | undefined,
) {}
value() {
return <T>(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<T>,
) => this.intercept(invocationCtx, next);
}

async intercept<T>(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<T>,
) {
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<T>(
invocationCtx: InvocationContext,
next: () => ValueOrPromise<T>,
) {
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;
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
});
Loading

0 comments on commit 8c9e8dc

Please sign in to comment.