Skip to content

Commit

Permalink
docs: migrate providers docs to VitePress
Browse files Browse the repository at this point in the history
  • Loading branch information
xavier-zenika committed Mar 8, 2024
1 parent 3e23364 commit 89876de
Show file tree
Hide file tree
Showing 20 changed files with 527 additions and 0 deletions.
230 changes: 230 additions & 0 deletions docs/docs/providers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
---
meta:
- name: description
content: Documentation over Providers / DI provided by Ts.ED framework. Use providers to build your backend services.
- name: keywords
content: providers di ioc ts.ed express typescript node.js javascript decorators jsonschema class models
---

# Providers

Basically, almost everything may be considered as a provider – service, factory, interceptors, and so on. All of them
can inject dependencies, meaning, they can create various relationships with each other. But in fact, a provider is
nothing else than just a simple class annotated with an `@Injectable()` decorator.

<figure><img src="./../assets/providers.png" style="max-height: 300px; padding: 5px"></figure>

In controllers chapter, we've seen how to build a Controller, handle a request and create a response. Controllers shall
handle HTTP requests and delegate complex tasks to the **providers**.

The providers are plain javascript class and use one of these decorators on top of them. Here is the list:

<ApiList query="['Injectable', 'Module', 'Service', 'Controller', 'Interceptor', 'JsonMapper', 'Middleware', 'Protocol'].indexOf(symbolName) > -1" />

## Services

Let's start by creating a simple CalendarService provider.

<<< @/docs/snippets/providers/getting-started-service.ts

::: tip Note

@@Service@@ and @@Injectable@@ have the same effect. @@Injectable@@ accepts options, @@Service@@ does not.
A Service is always configured as `singleton`.

Example with @@Injectable@:

<<< @/docs/snippets/providers/getting-started-injectable.ts

:::

Now we have the service class already done, let's use it inside the `CalendarsController`:

<<< @/docs/snippets/providers/getting-started-controller.ts

Finally, we can load the injector and use it:

<<< @/docs/snippets/providers/getting-started-serverloader.ts

::: tip NOTE

You'll notice that we only import the CalendarsController and not the CalendarsService as that would be the case
with other DIs (Angular / inversify). Ts.ED will discover automatically services/providers as soon as it is imported
into your application via an import ES6.

In most case, if a service is used by a controller or another service which is used by a controller, it's not necessary
to import it explicitly!
:::

## Dependency injection

Ts.ED is built around the **dependency injection** pattern. TypeScript emits type metadata on the constructor which will
be exploited by the @@InjectorService@@ to resolve dependencies automatically.

```typescript
import { Injectable } from "@tsed/di";

@Injectable()
class MyInjectable {
constructor(private calendarsService: CalendarsService) {}
}
```

It's also possible to inject a service on a property by using @@Inject@@ decorator:

```typescript
import { Injectable, Inject } from "@tsed/di";

@Injectable()
class MyInjectable {
@Inject()
private calendarsService: CalendarService;

$onInit() {
console.log(this.calendarsService);
}
}
```

In this case, the service won't be usable in the constructor. If you have to do something with the injected service,
you can use the `$onInit` hook.

## Scopes

All providers have a lifetime strictly dependent on the application lifecycle. Once the server is created, all providers
have to be instantiated. Similarly, when the application shuts down, all providers will be destroyed. However, there are
ways to make your provider lifetime **request-scoped** as well. You can read more about these
techniques [here](/docs/injection-scopes.md).

## Binding configuration

All configurations set with @@Module@@ or @@Configuration@@ can be retrieved with @@Constant@@ and @@Value@@ decorators.
These decorators can be used with:

- [Service](/docs/services.md),
- [Controller](/docs/controllers.md),
- [Middleware](/docs/middlewares.md),
- [Pipes](/docs/pipes.md).

@@Constant@@ and @@Value@@ accept an expression as parameter to inspect the configuration object and return the value.

<<< @/docs/snippets/providers/binding-configuration.ts

::: warning

@@Constant@@ returns an Object.freeze() value.
:::

::: tip NOTE

The values for the decorated properties aren't available on constructor. Use \$onInit() hook to use the
value.
:::

## Custom providers

The Ts.ED IoC resolves relationships providers for you, but sometimes, you want to tell to the DI how you want to
instantiate a specific service or inject different kind of providers based on values, on asynchronous or synchronous
factory or on external library. Look [here](/docs/custom-providers.md) to find more examples.

## Configurable provider

Sometimes you need to inject a provider with a specific configuration to another one.

This is possible with the combination of @@Opts@@ and @@UseOpts@@ decorators.

<<< @/docs/snippets/providers/configurable-provider.ts

::: warning

Using @@Opts@@ decorator on a constructor parameter changes the scope of the provider
to `ProviderScope.INSTANCE`.
:::

## Inject many provider <Badge text="6.129.0+"/>

This feature simplifies dependency management when working with multiple implementations of the same interface using type code.

If users use the same token when registering providers, the IoC container should exchange a token for a list of instances. Let's consider the following real example:

```typescript
interface Bar {
type: string;
}

const Bar: unique symbol = Symbol("Bar");

@Injectable({ type: Bar })
class Foo implements Bar {
private readonly type = "foo";
}

@Injectable({ type: Bar })
class Baz implements Bar {
private readonly type = "baz";
}
```

Now as a user, I would like to create a [registry](https://www.martinfowler.com/eaaCatalog/registry.html) and retrieve an appropriate instance by type:

```typescript
@Controller("/some")
export class SomeController {
constructor(@Inject(Bar) private readonly bars: Bar[]) {}

@Post()
async create(@Body("type") type: "baz" | "foo") {
const bar: Bar | undefined = this.bars.find((x) => x.type === type);
}
}
```

or in the following way as well:

```typescript
@Controller("/some")
export class SomeController {
constructor(private readonly injector: InjectorService) {}

@Post()
async create(@Body("type") type: "baz" | "foo") {
const bars: Bar[] = this.injector.getAll<Bar>(Bar);
const bar: Bar | undefined = bars.find((x) => x.type === type);

// your code
}
}
```

## Override an injection token <Badge text="6.93.0+"/>

By default, the `@Injectable()` decorator registers a class provider using an injection token obtained from the metadata generated by TypeScript.
That means that you have to use a concrete class as a token to resolve a provider.

To override an injection token, that is needed to resolve an instance, use the `@Injectable` decorator like this:

<<< @/docs/snippets/providers/override-injection-token.ts

An injection token may be either a string, a symbol, a class constructor.

> Just don't forget to import your provider in your project !
## Lazy load provider <Badge text="6.81.0+"/>

By default, modules are eagerly loaded, which means that as soon as the application loads, so do all the modules,
whether or not they are immediately necessary. While this is fine for most applications,
it may become a bottleneck for apps running in the **serverless environment**, where the startup latency `("cold start")` is crucial.

Lazy loading can help decrease bootstrap time by loading only modules required by the specific serverless function invocation.
In addition, you could also load other modules asynchronously once the serverless function is "warm" to speed-up the bootstrap time for subsequent calls even further (deferred modules registration).

You can read more about these techniques [here](/docs/providers-lazy-loading.md).

## Override provider

Any provider (Provider, Service, Controller, Middleware, etc...) already registered by Ts.ED or third-party can be
overridden by your own class.

<<< @/docs/snippets/providers/override-provider.ts

> Just don't forget to import your provider in your project !
14 changes: 14 additions & 0 deletions docs/docs/snippets/providers/binding-configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {Constant, Value} from "@tsed/di";
import {Env} from "@tsed/core";

export class MyClass {
@Constant("env")
env: Env;

@Value("swagger.path")
swaggerPath: string;

$onInit() {
console.log(this.env);
}
}
26 changes: 26 additions & 0 deletions docs/docs/snippets/providers/configurable-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {Injectable, Opts, UseOpts} from "@tsed/di";

@Injectable()
class MyConfigurableService {
source: string;

constructor(@Opts options: any = {}) {
console.log("Hello ", options.source); // log: Hello Service1 then Hello Service2

this.source = options.source;
}
}

@Injectable()
class MyService1 {
constructor(@UseOpts({source: "Service1"}) service: MyConfigurableService) {
console.log(service.source); // log: Service1
}
}

@Injectable()
class MyService2 {
constructor(@UseOpts({source: "Service2"}) service: MyConfigurableService) {
console.log(service.source); // log: Service2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Configuration, registerProvider} from "@tsed/di";
import {DatabaseConnection} from "connection-lib";

export const CONNECTION = Symbol.for("CONNECTION");

registerProvider({
provide: CONNECTION,
deps: [Configuration],
async useAsyncFactory(settings: Configuration) {
const options = settings.get("myOptions");
const connection = new DatabaseConnection(options);

await connection.connect();

return connection;
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {EnvTypes} from "@tsed/core";
import {registerProvider} from "@tsed/di";

export class ConfigService {}

export class DevConfigService {}

registerProvider({
provide: ConfigService,
useClass: process.env.NODE_ENV === EnvTypes.PROD ? ConfigService : DevConfigService
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {Injectable} from "@tsed/di";
import {ConfigService} from "./ConfigService";

@Injectable()
export class MyService {
constructor(configService: ConfigService) {
console.log(process.env.NODE_ENV, configService); // DevConfigService or ConfigService
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {Configuration, registerProvider} from "@tsed/di";
import {DatabaseConnection} from "connection-lib";

export const CONNECTION = Symbol.for("CONNECTION");

registerProvider<DatabaseConnection>({
provide: CONNECTION,
deps: [Configuration],
useFactory(configuration: Configuration) {
const options = configuration.get<any>("myOptions");

return new DatabaseConnection(options);
},
hooks: {
$onDestroy(connection) {
return connection.close();
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {registerProvider} from "@tsed/di";
import {connection} from "connection-lib";

export const CONNECTION = Symbol.for("CONNECTION");

registerProvider({
provide: CONNECTION,
useValue: connection,
hooks: {
$onDestroy(connection: any) {
return connection.close();
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Inject, Injectable} from "@tsed/di";
import {CONNECTION} from "./connection";

@Injectable()
export class MyService {
constructor(@Inject(CONNECTION) connection: any) {}
}
20 changes: 20 additions & 0 deletions docs/docs/snippets/providers/getting-started-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {BodyParams} from "@tsed/platform-params";
import {Get, Post} from "@tsed/schema";
import {Controller} from "@tsed/di";
import {Calendar} from "../models/Calendar";
import {CalendarsService} from "../services/CalendarsService";

@Controller("/calendars")
export class CalendarsController {
constructor(private readonly calendarsService: CalendarsService) {}

@Post()
async create(@BodyParams() calendar: Calendar) {
this.calendarsService.create(calendar);
}

@Get()
async findAll(): Promise<Calendar[]> {
return this.calendarsService.findAll();
}
}
18 changes: 18 additions & 0 deletions docs/docs/snippets/providers/getting-started-injectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {Injectable, ProviderScope, ProviderType} from "@tsed/di";
import {Calendar} from "../models/Calendar";

@Injectable({
type: ProviderType.SERVICE,
scope: ProviderScope.SINGLETON
})
export class CalendarsService {
private readonly calendars: Calendar[] = [];

create(calendar: Calendar) {
this.calendars.push(calendar);
}

findAll(): Calendar[] {
return this.calendars;
}
}
Loading

0 comments on commit 89876de

Please sign in to comment.