diff --git a/docs/docs/providers.md b/docs/docs/providers.md index e69de29..1499094 100644 --- a/docs/docs/providers.md +++ b/docs/docs/providers.md @@ -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. + +
+ +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: + + + +## 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 + +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); + const bar: Bar | undefined = bars.find((x) => x.type === type); + + // your code + } +} +``` + +## Override an injection token + +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 + +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 ! diff --git a/docs/docs/snippets/providers/binding-configuration.ts b/docs/docs/snippets/providers/binding-configuration.ts new file mode 100644 index 0000000..317afd5 --- /dev/null +++ b/docs/docs/snippets/providers/binding-configuration.ts @@ -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); + } +} diff --git a/docs/docs/snippets/providers/configurable-provider.ts b/docs/docs/snippets/providers/configurable-provider.ts new file mode 100644 index 0000000..0f58898 --- /dev/null +++ b/docs/docs/snippets/providers/configurable-provider.ts @@ -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 + } +} diff --git a/docs/docs/snippets/providers/custom-provider-use-async-factory-declaration.ts b/docs/docs/snippets/providers/custom-provider-use-async-factory-declaration.ts new file mode 100644 index 0000000..9015ec3 --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-async-factory-declaration.ts @@ -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; + } +}); diff --git a/docs/docs/snippets/providers/custom-provider-use-class-declaration.ts b/docs/docs/snippets/providers/custom-provider-use-class-declaration.ts new file mode 100644 index 0000000..0110298 --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-class-declaration.ts @@ -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 +}); diff --git a/docs/docs/snippets/providers/custom-provider-use-class-usage.ts b/docs/docs/snippets/providers/custom-provider-use-class-usage.ts new file mode 100644 index 0000000..d97560a --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-class-usage.ts @@ -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 + } +} diff --git a/docs/docs/snippets/providers/custom-provider-use-factory-declaration.ts b/docs/docs/snippets/providers/custom-provider-use-factory-declaration.ts new file mode 100644 index 0000000..ea14604 --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-factory-declaration.ts @@ -0,0 +1,19 @@ +import {Configuration, registerProvider} from "@tsed/di"; +import {DatabaseConnection} from "connection-lib"; + +export const CONNECTION = Symbol.for("CONNECTION"); + +registerProvider({ + provide: CONNECTION, + deps: [Configuration], + useFactory(configuration: Configuration) { + const options = configuration.get("myOptions"); + + return new DatabaseConnection(options); + }, + hooks: { + $onDestroy(connection) { + return connection.close(); + } + } +}); diff --git a/docs/docs/snippets/providers/custom-provider-use-value-declaration.ts b/docs/docs/snippets/providers/custom-provider-use-value-declaration.ts new file mode 100644 index 0000000..8c7b91d --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-value-declaration.ts @@ -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(); + } + } +}); diff --git a/docs/docs/snippets/providers/custom-provider-use-value-usage.ts b/docs/docs/snippets/providers/custom-provider-use-value-usage.ts new file mode 100644 index 0000000..befc2e2 --- /dev/null +++ b/docs/docs/snippets/providers/custom-provider-use-value-usage.ts @@ -0,0 +1,7 @@ +import {Inject, Injectable} from "@tsed/di"; +import {CONNECTION} from "./connection"; + +@Injectable() +export class MyService { + constructor(@Inject(CONNECTION) connection: any) {} +} diff --git a/docs/docs/snippets/providers/getting-started-controller.ts b/docs/docs/snippets/providers/getting-started-controller.ts new file mode 100644 index 0000000..dc2207d --- /dev/null +++ b/docs/docs/snippets/providers/getting-started-controller.ts @@ -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 { + return this.calendarsService.findAll(); + } +} diff --git a/docs/docs/snippets/providers/getting-started-injectable.ts b/docs/docs/snippets/providers/getting-started-injectable.ts new file mode 100644 index 0000000..de64c72 --- /dev/null +++ b/docs/docs/snippets/providers/getting-started-injectable.ts @@ -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; + } +} diff --git a/docs/docs/snippets/providers/getting-started-serverloader.ts b/docs/docs/snippets/providers/getting-started-serverloader.ts new file mode 100644 index 0000000..a70e7eb --- /dev/null +++ b/docs/docs/snippets/providers/getting-started-serverloader.ts @@ -0,0 +1,9 @@ +import {Configuration} from "@tsed/di"; +import {CalendarsController} from "./controllers/CalendarsController"; + +@Configuration({ + mount: { + "/rest": [CalendarsController] + } +}) +export class Server {} diff --git a/docs/docs/snippets/providers/getting-started-service.ts b/docs/docs/snippets/providers/getting-started-service.ts new file mode 100644 index 0000000..54a76db --- /dev/null +++ b/docs/docs/snippets/providers/getting-started-service.ts @@ -0,0 +1,15 @@ +import {Injectable} from "@tsed/di"; +import {Calendar} from "../models/Calendar"; + +@Injectable() +export class CalendarsService { + private readonly calendars: Calendar[] = []; + + create(calendar: Calendar) { + this.calendars.push(calendar); + } + + findAll(): Calendar[] { + return this.calendars; + } +} diff --git a/docs/docs/snippets/providers/override-injection-token.ts b/docs/docs/snippets/providers/override-injection-token.ts new file mode 100644 index 0000000..6faa514 --- /dev/null +++ b/docs/docs/snippets/providers/override-injection-token.ts @@ -0,0 +1,21 @@ +import {Inject, Injectable} from "@tsed/di"; + +export interface RetryPolicy { + retry unknown>(task: T): Promise>; +} + +export const RetryPolicy: unique symbol = Symbol("RetryPolicy"); + +@Injectable({provide: RetryPolicy}) +export class TokenBucket implements RetryPolicy { + public retry unknown>(task: T): Promise> { + // ... + } +} + +@Injectable() +export class MyService { + constructor(@Inject(RetryPolicy) private readonly retryPolicy: RetryPolicy) { + // an instance of `TokenBucket` + } +} diff --git a/docs/docs/snippets/providers/override-provider.ts b/docs/docs/snippets/providers/override-provider.ts new file mode 100644 index 0000000..cf77c14 --- /dev/null +++ b/docs/docs/snippets/providers/override-provider.ts @@ -0,0 +1,10 @@ +import {OriginalService} from "@tsed/common"; +import {OverrideProvider} from "@tsed/di"; + +@OverrideProvider(OriginalService) +export class CustomMiddleware extends OriginalService { + public method() { + // Do something + return super.method(); + } +} diff --git a/docs/docs/snippets/providers/scope-chain-fail.ts b/docs/docs/snippets/providers/scope-chain-fail.ts new file mode 100644 index 0000000..e4a2470 --- /dev/null +++ b/docs/docs/snippets/providers/scope-chain-fail.ts @@ -0,0 +1,19 @@ +import {Get} from "@tsed/schema"; +import {Controller, Injectable, ProviderScope, Scope} from "@tsed/di"; + +@Injectable() +@Scope(ProviderScope.REQUEST) +export class MyService { + public rand = Math.random() * 100; +} + +@Controller("/") +@Scope(ProviderScope.SINGLETON) // SINGLETON avoid all Scope("request") annotation +export class MyController { + constructor(private myService: MyService) {} + + @Get("/random") + async getValue() { + return this.myService.rand; + } +} diff --git a/docs/docs/snippets/providers/scope-chain.ts b/docs/docs/snippets/providers/scope-chain.ts new file mode 100644 index 0000000..ae926ac --- /dev/null +++ b/docs/docs/snippets/providers/scope-chain.ts @@ -0,0 +1,19 @@ +import {Get} from "@tsed/schema"; +import {Controller, Injectable, ProviderScope, Scope} from "@tsed/di"; + +@Injectable() +@Scope(ProviderScope.REQUEST) +export class MyService { + public rand = Math.random() * 100; +} + +@Controller("/") +@Scope(ProviderScope.REQUEST) +export class MyController { + constructor(private myService: MyService) {} + + @Get("/random") + async getValue() { + return this.myService.rand; + } +} diff --git a/docs/docs/snippets/providers/scope-instance.ts b/docs/docs/snippets/providers/scope-instance.ts new file mode 100644 index 0000000..5d54ede --- /dev/null +++ b/docs/docs/snippets/providers/scope-instance.ts @@ -0,0 +1,23 @@ +import {Get} from "@tsed/schema"; +import {Controller, Injectable, ProviderScope, Scope} from "@tsed/di"; + +@Injectable() +@Scope(ProviderScope.INSTANCE) +export class MyInstanceService { + private rand = Math.random() * 100; + + @Get("/random") + async getValue() { + return this.rand; + } +} + +@Controller("/") +@Scope(ProviderScope.SINGLETON) +export class MyController { + constructor(instance1: MyInstanceService, instance2: MyInstanceService) { + console.log("IsSame", instance1 === instance2); + console.log("instance1", instance1.getValue()); + console.log("instance2", instance2.getValue()); + } +} diff --git a/docs/docs/snippets/providers/scope-request.ts b/docs/docs/snippets/providers/scope-request.ts new file mode 100644 index 0000000..acf44e9 --- /dev/null +++ b/docs/docs/snippets/providers/scope-request.ts @@ -0,0 +1,13 @@ +import {Get} from "@tsed/schema"; +import {Controller, ProviderScope, Scope} from "@tsed/di"; + +@Controller("/") +@Scope(ProviderScope.REQUEST) +export class MyController { + private rand = Math.random() * 100; + + @Get("/random") + async getValue() { + return this.rand; + } +} diff --git a/docs/docs/snippets/providers/scope-singleton.ts b/docs/docs/snippets/providers/scope-singleton.ts new file mode 100644 index 0000000..5a2ae01 --- /dev/null +++ b/docs/docs/snippets/providers/scope-singleton.ts @@ -0,0 +1,13 @@ +import {Get} from "@tsed/schema"; +import {Controller, ProviderScope, Scope} from "@tsed/di"; + +@Controller("/") +@Scope(ProviderScope.SINGLETON) // OPTIONAL, leaving this annotation a the same behavior +export class MyController { + private rand = Math.random() * 100; + + @Get("/random") + async getValue() { + return this.rand; + } +}