0

I'm working on an application that will interface with multiple e-commerce platforms. I have the client modules I need for each platform (e.g. ShopifyModule, WooCommerceModule), each of which contains a service that conforms to a common interface EcommerceInterface. The problem is that I'd like to abstract over these so I can make a call like ecommercePlatformService.getProvider(storeId).listOrdersForCustomer(customerId).

Nest.js allows you to dynamically instantiate custom classes, but I wouldn't have control over the dependencies of those classes. I'd like to customize the ConfigService passed to each module so that I don't have to rewrite them to support a multi-platform environment. In addition, I would lose the ability to have multiple related services within each platform's module.

Instead of calling moduleRef.create(ShopifyService), is there a way to dynamically generate an entire module? For instance:

@Injectable()
export class EcommercePlatformService implements OnModuleInit {
  private services: Array<EcommerceInterface>;
  constructor(
    private moduleRef: ModuleRef,
    private readonly configService: ConfigService
  ){}

  async onModuleInit() {
    // `getPlatformOptions` details omitted for brevity
    const platformOptions: Array<{ url: string; type: 'shopify' | 'woo' }> = this.getPlatformOptions();

    this.services = await Promise.all(platformOptions.map(async ({ type, url }) => {
      const token = getPlatformToken(url);

      // **This method does not exist but expresses what I'm trying to do**
      let module = await this.moduleRef.someMethodForDynamicallyRegisteringModule(
        (type === 'shopify')
          ? ShopifyModule.register(url, token)
          : WooCommerceModule.register(url, token)
      );
      return module.get<EcommerceInterface>(token);
    }));
  }

  // ...omitted for brevity...
}

Prior research

A much longer description of what I've tried, though it's probably not helpful

My first attempt directly accepted an options object:

// ecommerce-platform.module.ts
export function getPlatformToken(key: string) {
  return `EcommercePlatform(${key})`;
}

@Module({ providers: [EcommercePlatformService] })
export class EcommercePlatformModule {
  static register(platforms: Array<{ url: string; type: 'shopify' | 'woo' }>) {
    return {
      module: EcommercePlatformModule,

      // Dynamically generate a module for each platform config
      imports: platforms.map(({ url, type }) => {
        switch (type) {
          case 'shopify': return ShopifyModule.register(url, getPlatformToken(url));
          default: return WooCommerceModule.register(url, getPlatformToken(url));
        }
      }),
      providers: [
        { provide: 'ECOMMERCE_ALL_PLATFORMS', useValue: platforms },
        EcommercePlatformService
      ],
      exports: [ EcommercePlatformService ],
    }
  }
}

The only requirement here is that each platform module expose a static register method that accepts a key for the platform's service, allowing us to use that token to get a reference to the EcommerceInterface-compliant Service:

// shopify.module.ts
@Module({ /* ... */ })
export class ShopifyModule {
  static register(url: string, serviceToken: string) {
    return {
      module: ShopifyModule,
      // ...
      providers: [
        ShopifyService,
        { provide: serviceToken, useExisting: ShopifyService },
        // ...omitted for brevity...
      ],
    };
  }
}

This approach works (example repo here) but requires options to be hard-coded. What I'd like to be able to do is pull a list of platforms from my ConfigService. I've reviewed the answers to this question, and if there were some way to generate the list of Providers from within a service I'd be all set, but it appears that that's not the case.

Micael Levi
  • 2,358
  • 2
  • 10
  • 18
Ryan Kennedy
  • 3,092
  • 4
  • 29
  • 46

0 Answers0