-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(rest): apply global interceptors for handler routes
- Loading branch information
1 parent
d9a2211
commit 8c9e8dc
Showing
4 changed files
with
248 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
100 changes: 100 additions & 0 deletions
100
packages/rest/src/__tests__/acceptance/caching-interceptor/caching-interceptor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
122 changes: 122 additions & 0 deletions
122
...est/src/__tests__/acceptance/caching-interceptor/global-caching-interceptor.acceptance.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
}); |
Oops, something went wrong.