@loopback/example-greeting-app 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.prettierignore +2 -0
  2. package/.prettierrc +7 -0
  3. package/.vscode/settings.json +19 -0
  4. package/.vscode/tasks.json +29 -0
  5. package/CHANGELOG.md +552 -0
  6. package/LICENSE +25 -0
  7. package/README.md +61 -0
  8. package/dist/__tests__/integration/greeting-service.integration.d.ts +1 -0
  9. package/dist/__tests__/integration/greeting-service.integration.js +68 -0
  10. package/dist/__tests__/integration/greeting-service.integration.js.map +1 -0
  11. package/dist/application.d.ts +98 -0
  12. package/dist/application.js +29 -0
  13. package/dist/application.js.map +1 -0
  14. package/dist/caching-service.d.ts +64 -0
  15. package/dist/caching-service.js +133 -0
  16. package/dist/caching-service.js.map +1 -0
  17. package/dist/controllers/greeting.controller.d.ts +10 -0
  18. package/dist/controllers/greeting.controller.js +59 -0
  19. package/dist/controllers/greeting.controller.js.map +1 -0
  20. package/dist/controllers/index.d.ts +1 -0
  21. package/dist/controllers/index.js +9 -0
  22. package/dist/controllers/index.js.map +1 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.js +37 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interceptors/caching.interceptor.d.ts +7 -0
  27. package/dist/interceptors/caching.interceptor.js +49 -0
  28. package/dist/interceptors/caching.interceptor.js.map +1 -0
  29. package/dist/interceptors/index.d.ts +1 -0
  30. package/dist/interceptors/index.js +9 -0
  31. package/dist/interceptors/index.js.map +1 -0
  32. package/dist/keys.d.ts +6 -0
  33. package/dist/keys.js +13 -0
  34. package/dist/keys.js.map +1 -0
  35. package/dist/observers/cache.observer.d.ts +19 -0
  36. package/dist/observers/cache.observer.js +39 -0
  37. package/dist/observers/cache.observer.js.map +1 -0
  38. package/dist/observers/index.d.ts +1 -0
  39. package/dist/observers/index.js +9 -0
  40. package/dist/observers/index.js.map +1 -0
  41. package/dist/openapi-spec.d.ts +1 -0
  42. package/dist/openapi-spec.js +28 -0
  43. package/dist/openapi-spec.js.map +1 -0
  44. package/dist/types.d.ts +8 -0
  45. package/dist/types.js +7 -0
  46. package/dist/types.js.map +1 -0
  47. package/greeting-app.png +0 -0
  48. package/package.json +69 -0
  49. package/src/__tests__/integration/greeting-service.integration.ts +79 -0
  50. package/src/application.ts +27 -0
  51. package/src/caching-service.ts +146 -0
  52. package/src/controllers/greeting.controller.ts +50 -0
  53. package/src/controllers/index.ts +6 -0
  54. package/src/index.ts +33 -0
  55. package/src/interceptors/caching.interceptor.ts +55 -0
  56. package/src/interceptors/index.ts +6 -0
  57. package/src/keys.ts +14 -0
  58. package/src/observers/cache.observer.ts +34 -0
  59. package/src/observers/index.ts +6 -0
  60. package/src/openapi-spec.ts +28 -0
  61. package/src/types.ts +13 -0
  62. package/tsconfig.json +30 -0
@@ -0,0 +1,79 @@
1
+ // Copyright IBM Corp. 2019,2020. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ Client,
8
+ createRestAppClient,
9
+ expect,
10
+ givenHttpServerConfig,
11
+ } from '@loopback/testlab';
12
+ import {promisify} from 'util';
13
+ import {GreetingApplication} from '../..';
14
+ import {CACHING_SERVICE} from '../../keys';
15
+
16
+ describe('GreetingApplication', () => {
17
+ let app: GreetingApplication;
18
+ let client: Client;
19
+
20
+ before(givenRunningApplicationWithCustomConfiguration);
21
+ after(() => app.stop());
22
+
23
+ before(() => {
24
+ client = createRestAppClient(app);
25
+ });
26
+
27
+ it('gets a greeting in English', async function () {
28
+ const response = await client
29
+ .get('/greet/Raymond')
30
+ .set('Accept-Language', 'en')
31
+ .expect(200);
32
+ expect(response.body).to.containEql({
33
+ language: 'en',
34
+ greeting: 'Hello, Raymond!',
35
+ });
36
+ });
37
+
38
+ it('gets a greeting in Chinese', async function () {
39
+ const response = await client
40
+ .get('/greet/Raymond')
41
+ .set('Accept-Language', 'zh')
42
+ .expect(200);
43
+ expect(response.body).to.containEql({
44
+ language: 'zh',
45
+ greeting: 'Raymond,你好!',
46
+ });
47
+ });
48
+
49
+ it('gets a greeting from cache', async function () {
50
+ app.configure(CACHING_SERVICE).to({ttl: 100});
51
+ let response = await client
52
+ .get('/greet/Raymond')
53
+ .set('Accept-Language', 'en')
54
+ .expect(200);
55
+ const msg1 = response.body;
56
+ // Now the result should be cached
57
+ response = await client
58
+ .get('/greet/Raymond')
59
+ .set('Accept-Language', 'en')
60
+ .expect(200);
61
+ expect(response.body).to.eql(msg1);
62
+ // Cache should be expired now
63
+ await promisify(setTimeout)(200);
64
+ response = await client
65
+ .get('/greet/Raymond')
66
+ .set('Accept-Language', 'en')
67
+ .expect(200);
68
+ expect(response.body.timestamp).to.not.eql(msg1.timestamp);
69
+ });
70
+
71
+ async function givenRunningApplicationWithCustomConfiguration() {
72
+ app = new GreetingApplication({
73
+ rest: givenHttpServerConfig(),
74
+ });
75
+
76
+ // Start Application
77
+ await app.main();
78
+ }
79
+ });
@@ -0,0 +1,27 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {BootMixin} from '@loopback/boot';
7
+ import {ApplicationConfig, createBindingFromClass} from '@loopback/core';
8
+ import {GreetingComponent} from '@loopback/example-greeter-extension';
9
+ import {RestApplication} from '@loopback/rest';
10
+ import {CachingService} from './caching-service';
11
+ import {CachingInterceptor} from './interceptors';
12
+ import {CACHING_SERVICE} from './keys';
13
+
14
+ export class GreetingApplication extends BootMixin(RestApplication) {
15
+ constructor(config: ApplicationConfig = {}) {
16
+ super(config);
17
+ this.projectRoot = __dirname;
18
+ this.add(createBindingFromClass(CachingService, {key: CACHING_SERVICE}));
19
+ this.add(createBindingFromClass(CachingInterceptor));
20
+ this.component(GreetingComponent);
21
+ }
22
+
23
+ async main() {
24
+ await this.boot();
25
+ await this.start();
26
+ }
27
+ }
@@ -0,0 +1,146 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {BindingScope, config, ContextView, injectable} from '@loopback/core';
7
+ import debugFactory from 'debug';
8
+ import {Message} from './types';
9
+ const debug = debugFactory('greeter-extension');
10
+
11
+ /**
12
+ * Configuration for CachingService
13
+ */
14
+ export interface CachingServiceOptions {
15
+ // The time-to-live setting for a cache entry
16
+ ttl: number;
17
+ }
18
+
19
+ /**
20
+ * Message caching service
21
+ */
22
+ @injectable({scope: BindingScope.SINGLETON})
23
+ export class CachingService {
24
+ private timer: NodeJS.Timer;
25
+ private store: Map<string, Message> = new Map();
26
+
27
+ constructor(
28
+ @config.view()
29
+ private optionsView: ContextView<CachingServiceOptions>,
30
+ ) {
31
+ // Use a view so that we can listen on `refresh` events, which are emitted
32
+ // when the configuration binding is updated in the context.
33
+ optionsView.on('refresh', () => {
34
+ debug('Restarting the service as configuration changes...');
35
+ this.restart().catch(err => {
36
+ console.error('Cannot restart the caching service.', err);
37
+ process.exit(1);
38
+ });
39
+ });
40
+ }
41
+
42
+ // We intentionally mark all operations `async` to reflect the commonly used
43
+ // caching infrastructure even though our in-memory implementation is based
44
+ // on a `Map` that provides synchronous APIs.
45
+ /**
46
+ * Store a message in the cache
47
+ * @param key - Key for caching
48
+ * @param message - Message
49
+ */
50
+ async set(key: string, message: Message) {
51
+ this.store.set(key, message);
52
+ }
53
+
54
+ /**
55
+ * Load a message from the cache by key
56
+ * @param key - Key for caching
57
+ */
58
+ async get(key: string) {
59
+ const expired = await this.isExpired(key);
60
+ debug('Getting cache for %s', key, expired);
61
+ return expired ? undefined : this.store.get(key);
62
+ }
63
+
64
+ /**
65
+ * Delete a message from the cache by key
66
+ * @param key - Key for caching
67
+ */
68
+ async delete(key: string) {
69
+ return this.store.delete(key);
70
+ }
71
+
72
+ /**
73
+ * Clear the cache
74
+ */
75
+ async clear() {
76
+ this.store.clear();
77
+ }
78
+
79
+ /**
80
+ * Check if the cached item is expired by key
81
+ * @param key - Key for caching
82
+ * @param now - The current date
83
+ */
84
+ async isExpired(key: string, now = new Date()): Promise<boolean> {
85
+ const ttl = await this.getTTL();
86
+ const msg = this.store.get(key);
87
+ if (!msg) return true;
88
+ return now.getTime() - msg.timestamp.getTime() > ttl;
89
+ }
90
+
91
+ /**
92
+ * Get the TTL setting
93
+ */
94
+ async getTTL() {
95
+ const options = await this.optionsView.singleValue();
96
+ debug('Caching options: %j', options);
97
+ return options?.ttl ?? 5000;
98
+ }
99
+
100
+ /**
101
+ * Remove expired items from the cache
102
+ */
103
+ async sweep() {
104
+ debug('Sweeping cache...');
105
+ for (const key of this.store.keys()) {
106
+ if (await this.isExpired(key)) {
107
+ debug('Cache for %s is swept.', key);
108
+ await this.delete(key);
109
+ }
110
+ }
111
+ }
112
+
113
+ /**
114
+ * This method will be invoked when the application starts
115
+ */
116
+ async start(): Promise<void> {
117
+ debug('Starting caching service');
118
+ await this.clear();
119
+ const ttl = await this.getTTL();
120
+ debug('TTL: %d', ttl);
121
+ this.timer = setInterval(() => {
122
+ this.sweep().catch(console.warn);
123
+ }, ttl);
124
+ }
125
+
126
+ /**
127
+ * This method will be invoked when the application stops
128
+ */
129
+ async stop(): Promise<void> {
130
+ debug('Stopping caching service');
131
+ /* istanbul ignore if */
132
+ if (this.timer) {
133
+ clearInterval(this.timer);
134
+ }
135
+ await this.clear();
136
+ }
137
+
138
+ /**
139
+ * This method may be used to restart the service (and may be triggered by a
140
+ * 'refresh' event)
141
+ */
142
+ async restart(): Promise<void> {
143
+ await this.stop();
144
+ await this.start();
145
+ }
146
+ }
@@ -0,0 +1,50 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {inject} from '@loopback/core';
7
+ import {
8
+ GreetingService,
9
+ GREETING_SERVICE,
10
+ } from '@loopback/example-greeter-extension';
11
+ import {get, param, Request, RestBindings} from '@loopback/rest';
12
+ import {Message} from '../types';
13
+
14
+ /* istanbul ignore file */
15
+ export class GreetingController {
16
+ constructor(
17
+ @inject(GREETING_SERVICE) private greetingService: GreetingService,
18
+ @inject(RestBindings.Http.REQUEST) private request: Request,
19
+ ) {}
20
+
21
+ @get('/greet/{name}', {
22
+ responses: {
23
+ '200': {
24
+ description: '',
25
+ content: {
26
+ 'application/json': {
27
+ schema: {
28
+ type: 'object',
29
+ properties: {
30
+ timestamp: 'string',
31
+ language: 'string',
32
+ message: 'string',
33
+ },
34
+ },
35
+ },
36
+ },
37
+ },
38
+ },
39
+ })
40
+ async greet(@param.path.string('name') name: string): Promise<Message> {
41
+ const language: string =
42
+ this.request.acceptsLanguages(['en', 'zh']) || 'en';
43
+ const greeting = await this.greetingService.greet(language, name);
44
+ return {
45
+ timestamp: new Date(),
46
+ language,
47
+ greeting,
48
+ };
49
+ }
50
+ }
@@ -0,0 +1,6 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ export * from './greeting.controller';
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ // Copyright IBM Corp. 2019,2020. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ export * from './application';
7
+ export * from './keys';
8
+ export * from './types';
9
+
10
+ import {GreetingApplication} from './application';
11
+ export async function main() {
12
+ const config = {
13
+ rest: {
14
+ port: +(process.env.PORT ?? 3000),
15
+ host: process.env.HOST ?? '127.0.0.1',
16
+ openApiSpec: {
17
+ // useful when used with OpenAPI-to-GraphQL to locate your application
18
+ setServersFromRequest: true,
19
+ },
20
+ },
21
+ };
22
+ const app = new GreetingApplication(config);
23
+ await app.main();
24
+ const url = app.restServer.url;
25
+ console.log(`The service is running at ${url}/greet/world.`);
26
+ }
27
+
28
+ if (require.main === module) {
29
+ main().catch(err => {
30
+ console.error('Cannot start the application.', err);
31
+ process.exit(1);
32
+ });
33
+ }
@@ -0,0 +1,55 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {
7
+ asGlobalInterceptor,
8
+ inject,
9
+ injectable,
10
+ Interceptor,
11
+ InvocationContext,
12
+ InvocationResult,
13
+ Provider,
14
+ ValueOrPromise,
15
+ } from '@loopback/core';
16
+ import {RestBindings} from '@loopback/rest';
17
+ import debugFactory from 'debug';
18
+ import {CachingService} from '../caching-service';
19
+ import {CACHING_SERVICE} from '../keys';
20
+
21
+ const debug = debugFactory('greeter-extension');
22
+
23
+ @injectable(asGlobalInterceptor('caching'))
24
+ export class CachingInterceptor implements Provider<Interceptor> {
25
+ constructor(
26
+ @inject(CACHING_SERVICE) private cachingService: CachingService,
27
+ ) {}
28
+
29
+ value() {
30
+ return async (
31
+ ctx: InvocationContext,
32
+ next: () => ValueOrPromise<InvocationResult>,
33
+ ) => {
34
+ const httpReq = await ctx.get(RestBindings.Http.REQUEST, {
35
+ optional: true,
36
+ });
37
+ /* istanbul ignore if */
38
+ if (!httpReq) {
39
+ // Not http request
40
+ return next();
41
+ }
42
+ const key = httpReq.path;
43
+ const lang = httpReq.acceptsLanguages(['en', 'zh']) || 'en';
44
+ const cachingKey = `${lang}:${key}`;
45
+ const cachedResult = await this.cachingService.get(cachingKey);
46
+ if (cachedResult) {
47
+ debug('Cache found for %s %j', cachingKey, cachedResult);
48
+ return cachedResult;
49
+ }
50
+ const result = await next();
51
+ await this.cachingService.set(cachingKey, result);
52
+ return result;
53
+ };
54
+ }
55
+ }
@@ -0,0 +1,6 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ export * from './caching.interceptor';
package/src/keys.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {BindingKey} from '@loopback/core';
7
+ import {CachingService} from './caching-service';
8
+
9
+ /**
10
+ * Strongly-typed binding key for CachingService
11
+ */
12
+ export const CACHING_SERVICE = BindingKey.create<CachingService>(
13
+ 'services.CachingService',
14
+ );
@@ -0,0 +1,34 @@
1
+ // Copyright IBM Corp. 2019. All Rights Reserved.
2
+ // Node module: @loopback/example-greeting-app
3
+ // This file is licensed under the MIT License.
4
+ // License text available at https://opensource.org/licenses/MIT
5
+
6
+ import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
7
+ import {CachingService} from '../caching-service';
8
+ import {CACHING_SERVICE} from '../keys';
9
+
10
+ /**
11
+ * This class will be bound to the application as a `LifeCycleObserver` during
12
+ * `boot`
13
+ */
14
+ @lifeCycleObserver('caching')
15
+ export class CacheObserver implements LifeCycleObserver {
16
+ private timer: NodeJS.Timer;
17
+ constructor(
18
+ @inject(CACHING_SERVICE) private cachingService: CachingService,
19
+ ) {}
20
+
21
+ /**
22
+ * This method will be invoked when the application starts
23
+ */
24
+ async start(): Promise<void> {
25
+ await this.cachingService.start();
26
+ }
27
+
28
+ /**
29
+ * This method will be invoked when the application stops
30
+ */
31
+ async stop(): Promise<void> {
32
+ await this.cachingService.stop();
33
+ }
34
+ }