diff --git a/CODEOWNERS b/CODEOWNERS index fd5d65262d3c..023236e41a30 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -25,6 +25,7 @@ packages/service-proxy/* @raymondfeng packages/testlab/* @bajtos examples/todo/* @bajtos @hacksparrow examples/express-composition/* @nabdelgadir +examples/greeter-extension/* @raymondfeng examples/hello-world/* @b-admike examples/log-extension/* @hacksparrow examples/rpc-server/* @hacksparrow diff --git a/docs/site/MONOREPO.md b/docs/site/MONOREPO.md index a83ce425da63..725788c84499 100644 --- a/docs/site/MONOREPO.md +++ b/docs/site/MONOREPO.md @@ -15,6 +15,7 @@ The [loopback-next](https://github.com/strongloop/loopback-next) repository uses | [core](https://github.com/strongloop/loopback-next/tree/master/packages/core) | @loopback/core | Define and implement core constructs such as Application and Component | | [docs](https://github.com/strongloop/loopback-next/tree/master/docs) | @loopback/docs | Documentation files rendered at [https://loopback.io](https://loopback.io) | | [example-express-composition](https://github.com/strongloop/loopback-next/tree/master/examples/express-composition) | @loopback/example-express-composition | A simple Express application that uses LoopBack 4 REST API | +| [example-greeter-extension](https://github.com/strongloop/loopback-next/tree/master/examples/greeter-extension) | @loopback/example-greeter-extension | An example showing how to implement extension point/extension pattern using LoopBack 4 | | [example-hello-world](https://github.com/strongloop/loopback-next/tree/master/examples/hello-world) | @loopback/example-hello-world | A simple hello-world application using LoopBack 4 | | [example-log-extension](https://github.com/strongloop/loopback-next/tree/master/examples/log-extension) | @loopback/example-log-extension | An example showing how to write a complex log extension for LoopBack 4 | | [example-rpc-server](https://github.com/strongloop/loopback-next/tree/master/examples/rpc-server) | @loopback/example-rpc-server | An example RPC server and application to demonstrate the creation of your own custom server | diff --git a/examples/greeter-extension/.npmrc b/examples/greeter-extension/.npmrc new file mode 100644 index 000000000000..cafe685a112d --- /dev/null +++ b/examples/greeter-extension/.npmrc @@ -0,0 +1 @@ +package-lock=true diff --git a/examples/greeter-extension/.prettierignore b/examples/greeter-extension/.prettierignore new file mode 100644 index 000000000000..c6911da9e1e8 --- /dev/null +++ b/examples/greeter-extension/.prettierignore @@ -0,0 +1,2 @@ +dist +*.json diff --git a/examples/greeter-extension/.prettierrc b/examples/greeter-extension/.prettierrc new file mode 100644 index 000000000000..f58b81dd7be2 --- /dev/null +++ b/examples/greeter-extension/.prettierrc @@ -0,0 +1,6 @@ +{ + "bracketSpacing": false, + "singleQuote": true, + "printWidth": 80, + "trailingComma": "all" +} diff --git a/examples/greeter-extension/.vscode/settings.json b/examples/greeter-extension/.vscode/settings.json new file mode 100644 index 000000000000..4a90df17452c --- /dev/null +++ b/examples/greeter-extension/.vscode/settings.json @@ -0,0 +1,21 @@ +{ + "editor.rulers": [80], + "editor.tabCompletion": "on", + "editor.tabSize": 2, + "editor.trimAutoWhitespace": true, + "editor.formatOnSave": true, + + "files.exclude": { + "**/.DS_Store": true, + "**/.git": true, + "**/.hg": true, + "**/.svn": true, + "**/CVS": true, + "dist": true, + }, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, + + "tslint.ignoreDefinitionFiles": true, + "typescript.tsdk": "./node_modules/typescript/lib" +} diff --git a/examples/greeter-extension/.vscode/tasks.json b/examples/greeter-extension/.vscode/tasks.json new file mode 100644 index 000000000000..c3003aa764e3 --- /dev/null +++ b/examples/greeter-extension/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Watch and Compile Project", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "build:watch"], + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$tsc-watch" + }, + { + "label": "Build, Test and Lint", + "type": "shell", + "command": "npm", + "args": ["--silent", "run", "test:dev"], + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": ["$tsc", "$tslint5"] + } + ] +} diff --git a/examples/greeter-extension/LICENSE b/examples/greeter-extension/LICENSE new file mode 100644 index 000000000000..6c4a691c6bb6 --- /dev/null +++ b/examples/greeter-extension/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) Author 2018. All Rights Reserved. +Node module: @loopback/example-greeter-extension +This project is licensed under the MIT License, full text below. + +-------- + +MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/examples/greeter-extension/README.md b/examples/greeter-extension/README.md new file mode 100644 index 000000000000..d8f65df2fc87 --- /dev/null +++ b/examples/greeter-extension/README.md @@ -0,0 +1,352 @@ +# @loopback/example-greeter-extension + +This example project illustrates how to implement the +[Extension Point/extension pattern](https://wiki.eclipse.org/FAQ_What_are_extensions_and_extension_points%3F), +which promotes loose coupling and offers great extensibility. There are many use +cases in LoopBack 4 that fit into design pattern. For example: + +- `@loopback/boot` uses `BootStrapper` that delegates to `Booters` to handle + different types of artifacts +- `@loopback/rest` uses `RequestBodyParser` that finds the corresponding + `BodyParsers` to parse request body encoded in different media types + +## Overview + +We'll use the following scenario to walk through important steps to organize the +`greet` service that allows extensible languages - each of them being supported +by a `Greeter` extension. + +![greeters](greeters.png) + +Various constructs from LoopBack 4, such as `Context`, `@inject.*`, and +`Component` are used to build the service in an extensible fashion. + +## Define an extension point + +In our scenario, we want to allow other modules to extend or customize how +people are greeted in different languages. To achieve that, we declare the +`greeter` extension point, which declares a contract as TypeScript interfaces +that extensions must conform to. + +### Define interface for extensions + +An extension point interacts with unknown number of extensions. It needs to +define one or more interfaces as contracts that each extension must implement. + +```ts +/** + * Typically an extension point defines an interface as the contract for + * extensions to implement + */ +export interface Greeter { + language: string; + greet(name: string): string; +} +``` + +### Define class for the extension point + +Typically an extension point is defined as a TypeScript class and bound to a +context. In our case, we mark `GreetingService` as the extension point that +needs to access a list of greeters. + +```ts +/** + * An extension point for greeters that can greet in different languages + */ +export class GreetingService { + constructor( + /** + * Inject a getter function to fetch greeters (bindings tagged with + * 'greeter') + */ + @inject.getter(bindingTagFilter({extensionPoint: 'greeter'})) + private greeters: Getter, + ) {} + // ... +} +``` + +To customize metadata such as `id` for the extension point, we can use +`@extensionPoint` to decorate the class, such as: + +```ts +@extensionPoint('greeter') +export class GreetingService {} +``` + +#### Access extensions for a given extension point + +To simplify access to extensions for a given extension point, we use dependency +injection to receive a `getter` function that gives us a list of greeters. + +```ts +@extensionPoint('greeter') +export class GreetingService { + constructor( + /** + * Inject a getter function to fetch greeters (bindings tagged with + * 'greeter') + */ + @extensions() // Sugar for @inject.getter(filterByTag({extensionPoint: 'greeter'})) + private greeters: Getter, // ... + ) {} +} +``` + +Please note that it's possible to add/remove greeters after the extension point +is instantiated. The injected getter function always pick up the latest list of +greeters. For example: + +```ts +// Get the latest list of greeters +const greeters = await this.greeters(); +``` + +#### Implement the delegation logic + +Typically, the extension point implementation will get a list of registered +extensions. For example, when a person needs to be greeted in a specific +language, the code iterates through all greeters to find an instance that +matches the language. In this module, `GreetingService` implements the `greet` +operation which uses `findGreeter` to find a greeter and produces a greeting for +the given language. + +```ts +export class GreetingService { + // ... + /** + * Find a greeter that can speak the given language + * @param language Language code for the greeting + */ + async findGreeter(language: string) { + // Get the latest list of greeters + const greeters = await this.greeters(); + // Find a greeter that can speak the given language + return greeters.find(g => g.language === language); + } + + /** + * Greet in the given language + * @param language Language code + * @param name Name + */ + async greet(language: string, name: string): Promise { + let greeting: string = ''; + + const greeter = await this.findGreeter(language); + if (greeter) { + greeting = greeter.greet(name); + } else { + // Fall back to English + greeting = `Hello, ${name}`; + } + if (this.options && this.options.color) { + greeting = chalk.keyword(this.options.color)(greeting); + } + return greeting; + } +} +``` + +## Implement an extension + +Modules that want to connect to `greeter` extension point must implement +`Greeter` interface in their extension. The key attribute is that the +`GreetingService` being extended knows nothing about the module that is +connecting to it beyond the scope of that contract. This allows `greeters` built +by different individuals or companies to interact seamlessly, even without their +knowing much about one another. + +```ts +import {Greeter, asGreeter} from '../types'; +import {bind, inject} from '@loopback/context'; + +/** + * Options for the Chinese greeter + */ +export interface ChineseGreeterOptions { + // Name first, default to `true` + nameFirst: boolean; +} + +/** + * A greeter implementation for Chinese + */ +@bind(asGreeter) +export class ChineseGreeter implements Greeter { + language = 'zh'; + + constructor( + /** + * Inject the configuration for ChineseGreeter + */ + @inject('greeters.ChineseGreeter.options', {optional: true}) + private options: ChineseGreeterOptions = {nameFirst: true}, + ) {} + + greet(name: string) { + if (this.options && this.options.nameFirst === false) { + return `你好,${name}!`; + } + return `${name},你好!`; + } +} +``` + +Please note we use +[`@bind`](https://loopback.io/doc/en/lb4/Binding.html#configure-binding-attributes-for-a-class) +to customize how the class can be bound. In this case, `asGreeter` is a binding +template function, which is equivalent as configuring a binding with +`{extensionPoint: 'greeter'}` tag and in the `SINGLETON` scope. + +```ts +/** + * A binding template for greeter extensions + * @param binding + */ +export const asGreeter: BindingTemplate = binding => + binding.inScope(BindingScope.SINGLETON).tag({extensionPoint: 'greeter'}); +``` + +## Register an extension point + +To register an extension point, we simply bind the implementation class to a +`Context`. For example: + +```ts +app + .bind('services.GreetingService') + .toClass(GreetingService) + .inScope(BindingScope.SINGLETON); +``` + +**NOTE**: Your extension point may choose to use a different binding scope. + +The process can be automated with a component: + +```ts +import {createBindingFromClass} from '@loopback/context'; +import {Component} from '@loopback/core'; +import {GreetingService} from './greeting-service'; +import {GREETING_SERVICE} from './keys'; + +/** + * Define a component to register the greeter extension point and built-in + * extensions + */ +export class GreeterComponent implements Component { + bindings = [ + createBindingFromClass(GreetingService, { + key: GREETING_SERVICE, + }), + // ... + ]; +} +``` + +## Register extensions + +To connect an extension to an extension point, we just have to bind the +extension to the `Context` and tag the binding with +`{extensionPoint: 'greeter'}`. + +```ts +app + .bind('greeters.FrenchGreeter') + .toClass(FrenchGreeter) + .apply(asGreeter); +``` + +Or + +```ts +app.add(createBindingFromClass(FrenchGreeter)); +``` + +The registration can be done using a component too: + +```ts +export class GreeterComponent implements Component { + bindings = [ + // ... + createBindingFromClass(EnglishGreeter), + createBindingFromClass(ChineseGreeter), + ]; +} +``` + +## Configure an extension point + +Sometimes it's desirable to make the extension point configurable. Two steps are +involved to achieve that. + +1. Declare an injection for the configuration for your extension point class: + +```ts +export class GreetingService { + constructor( + // ... + private greeters: Getter, + /** + * An extension point should be able to receive its options via dependency + * injection. + */ + @configuration() // Sugar for @inject('services.GreetingService.options', {optional: true}) + private options?: GreeterExtensionPointOptions, + ) {} +} +``` + +2. Set configuration for the extension point + +```ts +// Configure the extension point +app.bind('services.GreetingService.options').to({color: 'blue'}); +``` + +## Configure an extension + +Some extensions also support customization. The approach is similar as how to +configure an extension point. + +1. Declare an injection for the configuration in the extension class + +```ts +export class ChineseGreeter implements Greeter { + language = 'zh'; + + constructor( + /** + * Inject the configuration for ChineseGreeter + */ + @inject('greeters.ChineseGreeter.options', {optional: true}) + private options: ChineseGreeterOptions = {nameFirst: true}, + ) {} +} +``` + +2. Set configuration for the extension + +```ts +// Configure the ChineseGreeter +app.bind('greeters.ChineseGreeter.options').to({nameFirst: false}); +``` + +## Contributions + +- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md) +- [Join the team](https://github.com/strongloop/loopback-next/issues/110) + +## Tests + +Run `npm test` from the root folder. + +## Contributors + +See +[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors). + +## License + +MIT diff --git a/examples/greeter-extension/greeters.png b/examples/greeter-extension/greeters.png new file mode 100644 index 000000000000..600633d7940b Binary files /dev/null and b/examples/greeter-extension/greeters.png differ diff --git a/examples/greeter-extension/index.d.ts b/examples/greeter-extension/index.d.ts new file mode 100644 index 000000000000..18078ab5428c --- /dev/null +++ b/examples/greeter-extension/index.d.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './dist'; diff --git a/examples/greeter-extension/index.js b/examples/greeter-extension/index.js new file mode 100644 index 000000000000..96b109ab9512 --- /dev/null +++ b/examples/greeter-extension/index.js @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +module.exports = require('./dist'); diff --git a/examples/greeter-extension/index.ts b/examples/greeter-extension/index.ts new file mode 100644 index 000000000000..1b15e24a91e8 --- /dev/null +++ b/examples/greeter-extension/index.ts @@ -0,0 +1,6 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-log-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './src'; diff --git a/examples/greeter-extension/package-lock.json b/examples/greeter-extension/package-lock.json new file mode 100644 index 000000000000..bd9518b0d28b --- /dev/null +++ b/examples/greeter-extension/package-lock.json @@ -0,0 +1,371 @@ +{ + "name": "@loopback/example-greeter-extension", + "version": "1.0.0-1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/debug": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-0.0.30.tgz", + "integrity": "sha512-orGL5LXERPYsLov6CWs3Fh6203+dXzJkR7OnddIr2514Hsecwc8xRpzCapshBbKFImCsvS/mk6+FWiN5LyZJAQ==", + "dev": true + }, + "@types/node": { + "version": "10.12.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.30.tgz", + "integrity": "sha512-nsqTN6zUcm9xtdJiM9OvOJ5EF0kOI8f1Zuug27O/rgtxCRJHGqncSWfCMZUP852dCKPsDsYXGvBhxfRjDBkF5Q==", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "^1.1.3", + "esutils": "^2.0.2", + "js-tokens": "^3.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "diff": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", + "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "js-yaml": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", + "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", + "dev": true + }, + "resolve": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "tslib": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz", + "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==", + "dev": true + }, + "tslint": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.13.1.tgz", + "integrity": "sha512-fplQqb2miLbcPhyHoMV4FU9PtNRbgmm/zI5d3SZwwmJQM6V0eodju+hplpyfhLWpmwrDNfNYU57uYRb8s0zZoQ==", + "dev": true, + "requires": { + "babel-code-frame": "^6.22.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^3.2.0", + "glob": "^7.1.1", + "js-yaml": "^3.7.0", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.27.2" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "typescript": { + "version": "3.3.3333", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", + "integrity": "sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + } + } +} diff --git a/examples/greeter-extension/package.json b/examples/greeter-extension/package.json new file mode 100644 index 000000000000..9aef4590ee80 --- /dev/null +++ b/examples/greeter-extension/package.json @@ -0,0 +1,60 @@ +{ + "name": "@loopback/example-greeter-extension", + "version": "1.0.0-1", + "description": "An example extension point/extensions for LoopBack 4", + "main": "index.js", + "engines": { + "node": ">=8.9" + }, + "scripts": { + "build:apidocs": "lb-apidocs", + "build": "lb-tsc es2017 --outDir dist", + "build:watch": "lb-tsc es2017 --outDir dist --watch", + "clean": "lb-clean *example-greeter-extension-*.tgz dist package api-docs", + "lint": "npm run prettier:check && npm run tslint", + "lint:fix": "npm run tslint:fix && npm run prettier:fix", + "prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"", + "prettier:check": "npm run prettier:cli -- -l", + "prettier:fix": "npm run prettier:cli -- --write", + "tslint": "lb-tslint", + "tslint:fix": "npm run tslint -- --fix", + "pretest": "npm run clean && npm run build", + "test": "lb-mocha \"dist/__tests__/**/*.js\"", + "posttest": "npm run lint", + "test:dev": "lb-mocha --allow-console-logs dist/__tests__/**/*.js && npm run posttest", + "verify": "npm pack && tar xf *example-greeter-extension*.tgz && tree package && npm run clean" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/strongloop/loopback-next.git" + }, + "publishConfig": { + "access": "public" + }, + "keywords": [ + "loopback", + "loopback-extension" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/strongloop/loopback-next/issues" + }, + "homepage": "https://github.com/strongloop/loopback-next/tree/master/examples/greeter-extension", + "devDependencies": { + "@loopback/build": "^1.1.0", + "@loopback/testlab": "^1.0.3", + "@loopback/tslint-config": "^2.0.0", + "@types/debug": "0.0.30", + "@types/node": "^10.11.2", + "tslint": "^5.12.0", + "typescript": "^3.2.2" + }, + "dependencies": { + "@loopback/context": "^1.4.0", + "@loopback/core": "^1.1.3", + "@loopback/openapi-v3": "^1.1.5", + "@loopback/rest": "^1.5.1", + "chalk": "^2.4.2", + "debug": "^4.0.1" + } +} diff --git a/examples/greeter-extension/src/__tests__/greeter-extension.acceptance.ts b/examples/greeter-extension/src/__tests__/greeter-extension.acceptance.ts new file mode 100644 index 000000000000..6a86a1d224b3 --- /dev/null +++ b/examples/greeter-extension/src/__tests__/greeter-extension.acceptance.ts @@ -0,0 +1,80 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Application, bind, createBindingFromClass} from '@loopback/core'; +import {expect} from '@loopback/testlab'; +import chalk from 'chalk'; +import { + asGreeter, + Greeter, + GreeterComponent, + GreetingService, + GREETING_SERVICE, +} from '..'; + +describe('greeter-extension-pont', () => { + let app: Application; + let greetingService: GreetingService; + + beforeEach(givenAppWithGreeterComponent); + beforeEach(findGreetingService); + + it('greets by language', async () => { + let msg = await greetingService.greet('en', 'Raymond'); + expect(msg).to.eql('Hello, Raymond'); + msg = await greetingService.greet('zh', 'Raymond'); + expect(msg).to.eql('Raymond,你好!'); + }); + + it('supports options for the extension point', async () => { + // Configure the extension point + app.bind('services.GreetingService.options').to({color: 'blue'}); + const _greetingService = await app.get(GREETING_SERVICE); + const msg = await _greetingService.greet('en', 'Raymond'); + expect(msg).to.eql(chalk.keyword('blue')('Hello, Raymond')); + }); + + it('supports options for extensions', async () => { + // Configure the ChineseGreeter + app.bind('greeters.ChineseGreeter.options').to({nameFirst: false}); + const msg = await greetingService.greet('zh', 'Raymond'); + expect(msg).to.eql('你好,Raymond!'); + }); + + it('honors a newly added/removed greeter binding', async () => { + /** + * A greeter implementation for French + */ + @bind(asGreeter) + class FrenchGreeter implements Greeter { + language = 'fr'; + + greet(name: string) { + return `Bonjour, ${name}`; + } + } + // Add a new greeter for French + const binding = createBindingFromClass(FrenchGreeter); + app.add(binding); + + let msg = await greetingService.greet('fr', 'Raymond'); + expect(msg).to.eql('Bonjour, Raymond'); + + // Remove the French greeter + app.unbind(binding.key); + msg = await greetingService.greet('fr', 'Raymond'); + // It falls back to English + expect(msg).to.eql('Hello, Raymond'); + }); + + function givenAppWithGreeterComponent() { + app = new Application(); + app.component(GreeterComponent); + } + + async function findGreetingService() { + greetingService = await app.get(GREETING_SERVICE); + } +}); diff --git a/examples/greeter-extension/src/component.ts b/examples/greeter-extension/src/component.ts new file mode 100644 index 000000000000..b5f1381ef5f7 --- /dev/null +++ b/examples/greeter-extension/src/component.ts @@ -0,0 +1,25 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {createBindingFromClass} from '@loopback/context'; +import {Component} from '@loopback/core'; +import {GreetingService} from './greeting-service'; +import {ChineseGreeter} from './greeters/greeter-cn'; +import {EnglishGreeter} from './greeters/greeter-en'; +import {GREETING_SERVICE} from './keys'; + +/** + * Define a component to register the greeter extension point and built-in + * extensions + */ +export class GreeterComponent implements Component { + bindings = [ + createBindingFromClass(GreetingService, { + key: GREETING_SERVICE, + }), + createBindingFromClass(EnglishGreeter), + createBindingFromClass(ChineseGreeter), + ]; +} diff --git a/examples/greeter-extension/src/decorators.ts b/examples/greeter-extension/src/decorators.ts new file mode 100644 index 000000000000..3cda6cbc137f --- /dev/null +++ b/examples/greeter-extension/src/decorators.ts @@ -0,0 +1,89 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import { + ClassDecoratorFactory, + ContextView, + filterByTag, + inject, + MetadataInspector, +} from '@loopback/context'; + +export const EXTENSION_POINT_NAME = 'extensionPoint.name'; + +/** + * Decorate a class as a named extension point. If the decoration is not + * present, the name of the class will be used. + * + * TODO: to be promoted to `@loopback/core` module. + * @param name Name of the extension point + */ +export function extensionPoint(name: string) { + return ClassDecoratorFactory.createDecorator(EXTENSION_POINT_NAME, {name}); +} + +/** + * Shortcut to inject extensions for the given extension point. + * + * TODO: to be promoted to `@loopback/core` module possibly as + * `@inject.extensions`. + * + * @param extensionPoint Name of the extension point. If not supplied, we use + * the name from `@extensionPoint` or the class name of the extension point. + */ +export function extensions(extensionPointName?: string) { + return inject('', {decorator: '@extensions'}, (ctx, injection, session?) => { + if (!session) return undefined; + // Find the key of the target binding + if (!session.currentBinding) return undefined; + + if (!extensionPointName) { + let target: Function; + if (typeof injection.target === 'function') { + // Constructor injection + target = injection.target; + } else { + // Injection on the prototype + target = injection.target.constructor; + } + const meta: + | {name: string} + | undefined = MetadataInspector.getClassMetadata( + EXTENSION_POINT_NAME, + target, + ); + extensionPointName = + (meta && meta.name) || injection.target.constructor.name; + } + + const bindingFilter = filterByTag({extensionPoint: extensionPointName}); + const view = new ContextView(ctx, bindingFilter); + view.open(); + return view.asGetter(session); + }); +} + +/** + * Shortcut to inject configuration for the target binding. To be promoted + * as `@inject.config` in `@loopback/context` module. + * + * See https://github.com/strongloop/loopback-next/pull/2259 + */ +export function configuration() { + return inject( + '', + {decorator: '@inject.config', optional: true}, + (ctx, injection, session?) => { + if (!session) return undefined; + // Find the key of the target binding + if (!session.currentBinding) return undefined; + const key = session.currentBinding!.key; + return ctx.get(`${key}.options`, { + session, + optional: injection.metadata && injection.metadata.optional, + }); + }, + ); +} diff --git a/examples/greeter-extension/src/greeters/greeter-cn.ts b/examples/greeter-extension/src/greeters/greeter-cn.ts new file mode 100644 index 000000000000..74d4960ae713 --- /dev/null +++ b/examples/greeter-extension/src/greeters/greeter-cn.ts @@ -0,0 +1,38 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {bind, inject} from '@loopback/context'; +import {asGreeter, Greeter} from '../types'; + +/** + * Options for the Chinese greeter + */ +export interface ChineseGreeterOptions { + // Name first, default to `true` + nameFirst: boolean; +} + +/** + * A greeter implementation for Chinese. + */ +@bind(asGreeter) +export class ChineseGreeter implements Greeter { + language = 'zh'; + + constructor( + /** + * Inject the configuration for ChineseGreeter + */ + @inject('greeters.ChineseGreeter.options', {optional: true}) + private options: ChineseGreeterOptions = {nameFirst: true}, + ) {} + + greet(name: string) { + if (this.options && this.options.nameFirst === false) { + return `你好,${name}!`; + } + return `${name},你好!`; + } +} diff --git a/examples/greeter-extension/src/greeters/greeter-en.ts b/examples/greeter-extension/src/greeters/greeter-en.ts new file mode 100644 index 000000000000..47e404bbace8 --- /dev/null +++ b/examples/greeter-extension/src/greeters/greeter-en.ts @@ -0,0 +1,19 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {bind} from '@loopback/context'; +import {asGreeter, Greeter} from '../types'; + +/** + * A greeter implementation for English + */ +@bind(asGreeter) +export class EnglishGreeter implements Greeter { + language = 'en'; + + greet(name: string) { + return `Hello, ${name}`; + } +} diff --git a/examples/greeter-extension/src/greeting-service.ts b/examples/greeter-extension/src/greeting-service.ts new file mode 100644 index 000000000000..898b43d89c56 --- /dev/null +++ b/examples/greeter-extension/src/greeting-service.ts @@ -0,0 +1,69 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {Getter} from '@loopback/context'; +import chalk from 'chalk'; +import {configuration, extensionPoint, extensions} from './decorators'; +import {Greeter} from './types'; + +/** + * Options for the greeter extension point + */ +export interface GreetingServiceOptions { + color: string; +} + +/** + * An extension point for greeters that can greet in different languages + */ +@extensionPoint('greeter') +export class GreetingService { + constructor( + /** + * Inject a getter function to fetch greeters (bindings tagged with + * 'greeter') + */ + @extensions() // Sugar for @inject.getter(filterByTag({extensionPoint: 'greeter'})) + private greeters: Getter, + /** + * An extension point should be able to receive its options via dependency + * injection. + */ + @configuration() // Sugar for @inject('services.GreetingService.options', {optional: true}) + private options?: GreetingServiceOptions, + ) {} + + /** + * Find a greeter that can speak the given language + * @param language Language code for the greeting + */ + async findGreeter(language: string) { + // Get the latest list of greeters + const greeters = await this.greeters(); + // Find a greeter that can speak the given language + return greeters.find(g => g.language === language); + } + + /** + * Greet in the given language + * @param language Language code + * @param name Name + */ + async greet(language: string, name: string): Promise { + let greeting: string = ''; + + const greeter = await this.findGreeter(language); + if (greeter) { + greeting = greeter.greet(name); + } else { + // Fall back to English + greeting = `Hello, ${name}`; + } + if (this.options && this.options.color) { + greeting = chalk.keyword(this.options.color)(greeting); + } + return greeting; + } +} diff --git a/examples/greeter-extension/src/index.ts b/examples/greeter-extension/src/index.ts new file mode 100644 index 000000000000..3014eb83dd43 --- /dev/null +++ b/examples/greeter-extension/src/index.ts @@ -0,0 +1,9 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-log-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +export * from './component'; +export * from './greeting-service'; +export * from './keys'; +export * from './types'; diff --git a/examples/greeter-extension/src/keys.ts b/examples/greeter-extension/src/keys.ts new file mode 100644 index 000000000000..2a02ed0e9957 --- /dev/null +++ b/examples/greeter-extension/src/keys.ts @@ -0,0 +1,14 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-log-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingKey} from '@loopback/core'; +import {GreetingService} from './greeting-service'; + +/** + * Strongly-typed binding key for GreetingService + */ +export const GREETING_SERVICE = BindingKey.create( + 'services.GreetingService', +); diff --git a/examples/greeter-extension/src/types.ts b/examples/greeter-extension/src/types.ts new file mode 100644 index 000000000000..c20e8843c686 --- /dev/null +++ b/examples/greeter-extension/src/types.ts @@ -0,0 +1,33 @@ +// Copyright IBM Corp. 2019. All Rights Reserved. +// Node module: @loopback/example-greeter-extension +// This file is licensed under the MIT License. +// License text available at https://opensource.org/licenses/MIT + +import {BindingScope, BindingTemplate} from '@loopback/context'; + +/** + * Typically an extension point defines an interface as the contract for + * extensions to implement + */ +export interface Greeter { + language: string; + greet(name: string): string; +} + +/** + * A factory function to create binding template for extensions of the given + * extension point + * @param extensionPoint Name/id of the extension point + */ +export function extensionFor(extensionPoint: string): BindingTemplate { + return binding => + binding.inScope(BindingScope.SINGLETON).tag({extensionPoint}); +} + +/** + * A binding template for greeter extensions + */ +export const asGreeter: BindingTemplate = binding => { + extensionFor('greeter')(binding); + binding.tag({namespace: 'greeters'}); +}; diff --git a/examples/greeter-extension/tsconfig.build.json b/examples/greeter-extension/tsconfig.build.json new file mode 100644 index 000000000000..6e15e4be4f6f --- /dev/null +++ b/examples/greeter-extension/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + "extends": "@loopback/build/config/tsconfig.common.json", + "compilerOptions": { + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/examples/greeter-extension/tslint.build.json b/examples/greeter-extension/tslint.build.json new file mode 100644 index 000000000000..121b8adb21a3 --- /dev/null +++ b/examples/greeter-extension/tslint.build.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/tslint", + "extends": ["@loopback/tslint-config/tslint.build.json"] +} diff --git a/examples/greeter-extension/tslint.json b/examples/greeter-extension/tslint.json new file mode 100644 index 000000000000..2bb931e66a64 --- /dev/null +++ b/examples/greeter-extension/tslint.json @@ -0,0 +1,4 @@ +{ + "$schema": "http://json.schemastore.org/tslint", + "extends": ["@loopback/tslint-config/tslint.common.json"] +} diff --git a/greenkeeper.json b/greenkeeper.json index 158cf7ba1118..90726f094cf9 100644 --- a/greenkeeper.json +++ b/greenkeeper.json @@ -6,6 +6,7 @@ "benchmark/package.json", "docs/package.json", "examples/express-composition/package.json", + "examples/greeter-extension/package.json", "examples/hello-world/package.json", "examples/log-extension/package.json", "examples/rpc-server/package.json",