Skip to content

Dynamic registeration of plugins #144

Closed

Description

@radhay-1199

Problem statement: Everyone can create a plugin which is again a class with methods to generate a doc. Now currently if I do so I have to go to my factory and update the code there for the new plugin and in the app module too I have to go and add that as the provider. but I don't want this to be so much hassle.
Proposed solution:

  • some annotation over these plugin classes with tokens. I can use this token itself in the factory class to return the correct instance of the plugin. and for the app module, I can scan all the classes with my annotation and populate them in providers during runtime only. now i won't have to go and change any existing file if I add a new plugin

Activity

  1. radhay-1199 commented on May 28, 2023

    @radhay-1199
    CollaboratorAuthor

    POC:

    • Define an annotation decorator for your plugins:
    // plugin.decorator.ts
    import { SetMetadata } from '@nestjs/common';
    
    export const PLUGIN_TOKEN = 'PLUGIN_TOKEN';
    
    export const Plugin = (token: string): ClassDecorator => {
      return SetMetadata(PLUGIN_TOKEN, token);
    };
    • Use the Plugin decorator to mark your plugin classes with the corresponding token:
    // pdf-plugin.ts
    import { Plugin } from './plugin.decorator';
    
    @Plugin('pdf-plugin')
    export class PdfPlugin {
      generateDoc(): string {
        // Logic to generate a PDF document
        return 'Generated PDF document';
      }
    }
    • Create a factory class that will instantiate the plugins based on their tokens:
    // plugin.factory.ts
    import { Injectable } from '@nestjs/common';
    import { PLUGIN_TOKEN } from './plugin.decorator';
    
    @Injectable()
    export class PluginFactory {
      private plugins: Map<string, any> = new Map();
    
      registerPlugin(token: string, plugin: any) {
        this.plugins.set(token, plugin);
      }
    
      createPlugin(token: string): any {
        const plugin = this.plugins.get(token);
        if (!plugin) {
          throw new Error(`Plugin not found for token: ${token}`);
        }
        return new plugin();
      }
    }
    • Scan for classes with the Plugin decorator in your app module and register them dynamically:
    // app.module.ts
    import { Module, ModuleRef, OnModuleInit, Reflector } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { PluginFactory } from './plugin.factory';
    import { PLUGIN_TOKEN } from './plugin.decorator';
    
    @Module({
      controllers: [AppController],
      providers: [PluginFactory],
    })
    export class AppModule implements OnModuleInit {
      constructor(
        private readonly moduleRef: ModuleRef,
        private readonly reflector: Reflector,
        private readonly pluginFactory: PluginFactory,
      ) {}
    
      onModuleInit() {
        const providers = this.moduleRef.getProviders();
        providers.forEach((provider) => {
          const pluginToken = this.reflector.get<string>(PLUGIN_TOKEN, provider.metatype);
          if (pluginToken) {
            const pluginInstance = this.moduleRef.get(provider.metatype);
            this.pluginFactory.registerPlugin(pluginToken, pluginInstance);
          }
        });
      }
    }
    • Create a controller that injects the PluginFactory and uses the plugins:
    // doc.controller.ts
    import { Controller, Get, Param, Inject } from '@nestjs/common';
    import { PluginFactory } from './plugin.factory';
    
    @Controller('documents')
    export class DocController {
      constructor(
        @Inject(PluginFactory) private readonly pluginFactory: PluginFactory,
      ) {}
    
      @Get(':plugin/')
      generateDocument(@Param('plugin') pluginToken: string): string {
        const plugin = this.pluginFactory.createPlugin(pluginToken);
        const generatedDoc = plugin.generateDoc();
        return generatedDoc;
      }
    }

    With this approach, we can create new plugin classes with the Plugin decorator and their respective tokens. They will be automatically discovered and registered in the PluginFactory during the runtime of your NestJS application.

  2. radhay-1199 commented on May 28, 2023

    @radhay-1199
    CollaboratorAuthor

    Long term goals: Implement awilix DI for plugins with scoped lifetime management for each plugins
    References- https://github.com/jeffijoe/awilix

  3. yuvrajsab commented on May 28, 2023

    @yuvrajsab
    Member

    @radhay-1199 but here

      createPlugin(token: string): any {
        const plugin = this.plugins.get(token);
        if (!plugin) {
          throw new Error(`Plugin not found for token: ${token}`);
        }
        return new plugin();
      }

    we are newing up the plugin instance whenever a request comes. These plugin classes should be singleton.

  4. AnshulMalik commented on May 29, 2023

    @AnshulMalik
    Collaborator

    Looks good, agree with @yuvrajsab , let's make don't make these objects if already have one.

    Now this is out of they way, we need to work on the signature of the input and output plugin methods.

  5. radhay-1199 commented on May 29, 2023

    @radhay-1199
    CollaboratorAuthor

    @yuvrajsab No actually, this map will already have beans associated with the name in them. NestJs assures that threads are singleton only.

  6. yuvrajsab commented on May 29, 2023

    @yuvrajsab
    Member

    okay and @radhay-1199 one more question ki if a plugin class needs DI for let's say ConfigService or any other service then how are we gonna inject that or does this approach already handle that.

  7. locked and limited conversation to collaborators on Jun 4, 2023
  8. converted this issue into a discussion #154 on Jun 4, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions