Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: update dependency injection examples #3024

Merged
merged 1 commit into from
Jun 13, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 67 additions & 37 deletions docs/site/Dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,62 @@ technique where the construction of dependencies of a class or function is
separated from its behavior, in order to keep the code
[loosely coupled](https://en.wikipedia.org/wiki/Loose_coupling).

For example, the Sequence Action `authenticate` supports different
authentication strategies (e.g. HTTP Basic Auth, OAuth2, etc.). Instead of
hard-coding some sort of a lookup table to find the right strategy instance,
`authenticate` uses dependency injection to let the caller specify which
strategy to use.
For example, the Sequence Action `authenticate` in `@loopback/authentication`
supports different authentication strategies (e.g. HTTP Basic Auth, OAuth2,
etc.). Instead of hard-coding some sort of a lookup table to find the right
strategy instance, the `authenticate` action uses dependency injection to let
the caller specify which strategy to use.

The example below shows a simplified implementation of `authenticate` action,
please refer to the source code of `@loopback/authenticate` for the full working
version.
The implementation of the `authenticate` action is shown below.

```ts
class AuthenticateActionProvider {
constructor(@inject(AuthenticationBindings.STRATEGY) strategy) {
this.strategy = strategy;
}
export class AuthenticateActionProvider implements Provider<AuthenticateFn> {
constructor(
// The provider is instantiated for Sequence constructor,
// at which time we don't have information about the current
// route yet. This information is needed to determine
// what auth strategy should be used.
// To solve this, we are injecting a getter function that will
// defer resolution of the strategy until authenticate() action
// is executed.
@inject.getter(AuthenticationBindings.STRATEGY)
readonly getStrategy: Getter<AuthenticationStrategy>,
@inject.setter(AuthenticationBindings.CURRENT_USER)
readonly setCurrentUser: Setter<UserProfile>,
) {}

/**
* @returns AuthenticateFn
*/
value(): AuthenticateFn {
return request => this.action(request);
}

// this is the function invoked by "authenticate()" sequence action
action(request: Request) {
const adapter = new StrategyAdapter(this.strategy);
const user = await adapter.authenticate(request);
return user;
/**
* The implementation of authenticate() sequence action.
* @param request - The incoming request provided by the REST layer
*/
async action(request: Request): Promise<UserProfile | undefined> {
const strategy = await this.getStrategy();
if (!strategy) {
// The invoked operation does not require authentication.
return undefined;
}

const userProfile = await strategy.authenticate(request);
if (!userProfile) {
// important to throw a non-protocol-specific error here
let error = new Error(
`User profile not returned from strategy's authenticate function`,
);
Object.assign(error, {
code: USER_PROFILE_NOT_FOUND,
});
throw error;
}

this.setCurrentUser(userProfile);
return userProfile;
}
}
```
Expand All @@ -62,39 +93,38 @@ In LoopBack, we use [Context](Context.md) to keep track of all injectable
dependencies.

There are several different ways for configuring the values to inject, the
simplest options is to call `app.bind(key).to(value)`. Building on top of the
example above, one can configure the app to use a Basic HTTP authentication
strategy as follows:
simplest options is to call `app.bind(key).to(value)`.

```ts
// TypeScript example
export namespace JWTAuthenticationStrategyBindings {
export const TOKEN_SECRET = BindingKey.create<string>(
'authentication.strategy.jwt.secret',
);
export const TOKEN_EXPIRES_IN = BindingKey.create<string>(
'authentication.strategy.jwt.expires.in.seconds',
);
}

import {BasicStrategy} from 'passport-http';
import {RestApplication, RestServer} from '@loopback/rest';
// basic scaffolding stuff happens in between...
...

// The REST server has its own context!
const server = await app.getServer(RestServer);
server.bind(AuthenticationBindings.STRATEGY).to(new BasicStrategy(loginUser));
server
.bind(JWTAuthenticationStrategyBindings.TOKEN_SECRET)
.to('myjwts3cr3t');

function loginUser(username, password, cb) {
// check that username + password are valid
}
server
.bind(JWTAuthenticationStrategyBindings.TOKEN_EXPIRES_IN)
.to('600');
```

However, when you want to create a binding that will instantiate a class and
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emonddr One more thing to update here:

However, when you want to create a binding that will instantiate a configured class(as a provider) and automatically inject required dependencies, then you need to use .toProvider() method:

server
  .bind(AuthenticationBindings.AUTH_ACTION)
  .toProvider(AuthenticateActionProvider);

const provider = await server.get(AuthenticationBindings.AUTH_ACTION);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jannyHou , the existing document used the AuthenticateActionProvider in a .toClass() and in the .toProvider() example.

I've changed the .toClass() example code to use a different class instead; so 1) it is less confusing as to why different binding methods were used on the same class, and 2) so I don't have to change the explanatory paragraph:

image

automatically inject required dependencies, then you need to use `.toClass()`
method:

```ts
server
.bind(AuthenticationBindings.AUTH_ACTION)
.toClass(AuthenticateActionProvider);
server.bind(TokenServiceBindings.TOKEN_SERVICE).toClass(TokenService);

const provider = await server.get(AuthenticationBindings.AUTH_ACTION);
// provider is an AuthenticateActionProvider instance
// provider.strategy was set to the value returned
// by server.get('authentication.strategy')
const tokenService = await server.get(TokenServiceBindings.TOKEN_SERVICE);
// tokenService is a TokenService instance
```

When a binding is created via `.toClass()`, [Context](Context.md) will create a
Expand Down