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.