Samagra-Development / Doc-Generator

Create PDFs from a variety of formats.
17 stars 45 forks source link

Dynamic registeration of plugins #144

Closed radhay-1199 closed 1 year ago

radhay-1199 commented 1 year ago

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:

radhay-1199 commented 1 year ago

POC:

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:
```ts
// 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';
  }
}

@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:
```ts
// 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);
      }
    });
  }
}

@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.
radhay-1199 commented 1 year ago

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

yuvrajsab commented 1 year ago

@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.

AnshulMalik commented 1 year ago

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.

radhay-1199 commented 1 year ago

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

yuvrajsab commented 1 year ago

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.