|
1 | 1 | # Plugin API |
2 | 2 |
|
3 | | -:::warning |
| 3 | +:::info |
4 | 4 |
|
5 | | -The Plugin API is still experimental and might change in the future, you should not rely on it in production. |
| 5 | +The Plugin API is stable since `v6.0.0` and should only change between major versions. |
6 | 6 |
|
7 | | -Using any of the "official" plugins is safe since they are maintained by the same author and compatibility of new versions is ensured. If you want to create your own plugin, you should be aware that the API might change between minor versions. |
| 7 | +Any of the "official" plugins will always be kept in sync and updated to be compatible with any new version of the Plugin API. |
8 | 8 |
|
9 | 9 | ::: |
10 | 10 |
|
11 | | -A plugin is, in its core, a NestJS module with some extra options and should implement the following interface: |
| 11 | +## Overview |
| 12 | + |
| 13 | +A plugin provides a way to hook into the lifecycle of the `setup` phase of Cls-initializers (middleware, interceptor, guard, decorator) and modify/extend the contents of CLS Store. |
| 14 | + |
| 15 | +Every plugin must implement the `ClsPlugin` interface and have a **globally unique name** that is used to identify its components in the DI system. |
| 16 | + |
| 17 | +A plugin is, it its essence, just a NestJS module that can register its own providers and standard NestJS lifecycle hooks. These providers will be available in the DI system everywhere that `ClsService` is available. |
| 18 | + |
| 19 | +## Plugin interface |
12 | 20 |
|
13 | 21 | ```ts |
14 | | -export interface ClsPlugin { |
15 | | - /** |
16 | | - * The name of the plugin, used for logging and debugging |
17 | | - */ |
18 | | - name: string; |
19 | | - |
20 | | - /** |
21 | | - * Function that is called within a Cls initializer (middleware, interceptor, guard, etc.) |
22 | | - * right after `setup`. |
23 | | - */ |
24 | | - onClsInit?: (cls: ClsService) => void | Promise<void>; |
25 | | - |
26 | | - /** |
27 | | - * A lifecycle method called when the `ClsModule` is initialized |
28 | | - */ |
29 | | - onModuleInit?: () => void | Promise<void>; |
| 22 | +interface ClsPlugin { |
| 23 | + readonly name: string; |
| 24 | + |
| 25 | + imports?: any[]; |
| 26 | + providers?: Provider[]; |
| 27 | + exports?: any[]; |
30 | 28 |
|
31 | | - /** |
32 | | - * A lifecycle method called when the `ClsModule` is destroyed |
33 | | - * (only when shutdown hooks are enabled) |
34 | | - */ |
| 29 | + onModuleInit?: () => void | Promise<void>; |
35 | 30 | onModuleDestroy?: () => void | Promise<void>; |
| 31 | + onApplicationBootstrap?: () => void | Promise<void>; |
| 32 | + onApplicationShutdown?: (signal?: string) => void | Promise<void>; |
| 33 | + beforeApplicationShutdown?: (signal?: string) => void | Promise<void>; |
| 34 | +} |
| 35 | +``` |
36 | 36 |
|
37 | | - /** |
38 | | - * An array of external modules that should be imported for the plugin to work. |
39 | | - */ |
40 | | - imports?: any[]; |
| 37 | +## CLS Hooks |
41 | 38 |
|
42 | | - /** |
43 | | - * An array of providers that the plugin provides. |
44 | | - */ |
45 | | - providers?: Provider[]; |
| 39 | +As mentioned above, a plugin can register a special provider that implements the `ClsPluginHooks` interface. This provider should be registered under the `getPluginHooksToken(<pluginName>)` token, where `pluginName` is the name of the plugin. |
46 | 40 |
|
47 | | - /** |
48 | | - * An array of providers that the plugin provides that should be exported. |
49 | | - */ |
50 | | - exports?: any[]; |
| 41 | +```ts |
| 42 | +interface ClsPluginHooks { |
| 43 | + beforeSetup?: ( |
| 44 | + cls: ClsService, |
| 45 | + context: ClsInitContext, |
| 46 | + ) => void | Promise<void>; |
| 47 | + |
| 48 | + afterSetup?: ( |
| 49 | + cls: ClsService, |
| 50 | + context: ClsInitContext, |
| 51 | + ) => void | Promise<void>; |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +This interface can contain two methods: `beforeSetup` and `afterSetup`. These methods are called before and after the `setup` phase of the Cls-initializers and have access to the `ClsService` and the `ClsInitContext` object. |
| 56 | + |
| 57 | +Since the plugin cannot know which Cls-initializer is being used, it is up to the plugin to check the `ClsInitContext` object and decide what to do. The `ClsInitContext` will always contain the `kind` property, with a value of either `middleware`, `interceptor`, `guard`, `decorator` or `custom`. and other properties depending on the kind of Cls-initializer. |
| 58 | + |
| 59 | +A plugin author should indicate in the documentation which Cls-initializers are supported by the plugin, if there are any limitations. Otherwise, the plugin should be able to work with any Cls-initializer. |
| 60 | + |
| 61 | +## Creating a plugin |
| 62 | + |
| 63 | +Implementing the aforementioned interface and supplying the (optional) hooks provider is all that is needed to create a plugin. And instance of the plugin can be passed to the `plugins` array of the `ClsModule` options. |
| 64 | + |
| 65 | +However, the `nestjs-cls` package exports a `ClsPluginBase` class, that can be extended to easily create a plugin. |
| 66 | + |
| 67 | +In this example, we will implement a plugin that extracts the `user` property from the request and registers wraps it in a custom [Proxy provider](../03_features-and-use-cases/06_proxy-providers.md) for injection. |
| 68 | + |
| 69 | +The plugin will work in the following way: |
| 70 | + |
| 71 | +1. First, check if the `user` property is already set in the CLS Store. If it is, do nothing. |
| 72 | +2. Determine the kind of Cls-initializer that is being used and add the `user` property to the CLS Store. |
| 73 | +3. Register a custom `ClsUserHost` proxy provider that hosts the `user` property for injection anywhere in the application. |
| 74 | + |
| 75 | +```ts |
| 76 | +// Define a symbol to be used as a key in the CLS Store |
| 77 | +export const USER_CLS_SYMBOL = Symbol('user'); |
| 78 | + |
| 79 | +// Define a custom proxy provider that will be used to inject the user property |
| 80 | +@InjectableProxy() |
| 81 | +export class ClsUserHost { |
| 82 | + public readonly user: MyUserType; |
| 83 | + |
| 84 | + constructor(private readonly cls: ClsService) { |
| 85 | + this.user = this.cls.get(USER_CLS_SYMBOL); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +// To Create the plugin, extend the ClsPluginBase class |
| 90 | +export class UserPlugin extends ClsPluginBase { |
| 91 | + constructor() { |
| 92 | + // Specify a unique name for the plugin |
| 93 | + super('user-plugin'); |
| 94 | + |
| 95 | + // Register the plugin hooks using the convenience method |
| 96 | + this.registerHooks({ |
| 97 | + useFactory: () => ({ |
| 98 | + afterSetup(cls, context) { |
| 99 | + // This hook will be called after the setup phase of every Cls-initializer |
| 100 | + // so we check if the user property is already set and do nothing |
| 101 | + if (cls.has(USER_CLS_SYMBOL)) { |
| 102 | + return; |
| 103 | + } |
| 104 | + |
| 105 | + // If the user property is not set, we check the kind of Cls-initializer |
| 106 | + switch (context.kind) { |
| 107 | + case 'middleware': |
| 108 | + cls.set(USER_CLS_SYMBOL, context.req.user); |
| 109 | + break; |
| 110 | + case 'interceptor': |
| 111 | + cls.set( |
| 112 | + USER_CLS_SYMBOL, |
| 113 | + context.ctx.switchToHttp().getRequest().user, |
| 114 | + ); |
| 115 | + break; |
| 116 | + case 'guard': |
| 117 | + cls.set( |
| 118 | + USER_CLS_SYMBOL, |
| 119 | + context.ctx.switchToHttp().getRequest().user, |
| 120 | + ); |
| 121 | + break; |
| 122 | + default: |
| 123 | + // If the kind is not supported (decorator or custom), we throw an error, |
| 124 | + // because there is no request. |
| 125 | + // If the user wants to use the plugin in a decorator or a custom |
| 126 | + // Cls-initializer, they should set the user property manually |
| 127 | + // in the `setup` method of the Decorator |
| 128 | + throw new Error( |
| 129 | + `Unsupported context kind: ${context.kind}`, |
| 130 | + ); |
| 131 | + } |
| 132 | + }, |
| 133 | + }), |
| 134 | + }); |
| 135 | + |
| 136 | + // Register the custom Proxy provider |
| 137 | + this.imports.push(ClsModule.forFeature(ClsUserHost)); |
| 138 | + } |
51 | 139 | } |
52 | 140 | ``` |
53 | 141 |
|
54 | | -Each plugin creates a new instance of a _global_ `ClsPluginModule` and the exposed providers can be used for injection by other plugin-related code. |
| 142 | +:::info |
| 143 | + |
| 144 | +It is also possible to expose the User itself as Proxy provider without the need of the plugin. This is only for demonstration purposes. |
| 145 | + |
| 146 | +::: |
| 147 | + |
| 148 | +## Using plugin options |
| 149 | + |
| 150 | +If we wanted to customize the plugin and allow the user to be retrieved from a custom property name from the request, we could do it by adding some options to the plugin constructor. |
| 151 | + |
| 152 | +```ts |
| 153 | +export class UserPlugin extends ClsPluginBase { |
| 154 | + // highlight-start |
| 155 | + constructor(userPropertyName: string) { |
| 156 | + // highlight-end |
| 157 | + // Specify a unique name for the plugin |
| 158 | + super('user-plugin'); |
| 159 | + |
| 160 | +// [...] |
| 161 | + |
| 162 | + switch (context.kind) { |
| 163 | + case 'middleware': |
| 164 | + // highlight-start |
| 165 | + cls.set(USER_CLS_SYMBOL, context.req[userPropertyName]); |
| 166 | + // highlight-end |
| 167 | + break; |
| 168 | +``` |
| 169 | +
|
| 170 | +A more advanced use-case would be to allow passing the options asynchronously. For that, we can use the `imports` array and the `inject` method on the `this.registerHooks` method. An example can be found in the implementation of the existing plugins. |
0 commit comments