Skip to content

Commit

Permalink
feat(dynamic modules): helpers to reduce dynamic module boilerplate
Browse files Browse the repository at this point in the history
  • Loading branch information
WonderPanda committed Oct 29, 2019
1 parent 2bb6404 commit 80a2b2c
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 88 deletions.
Empty file.
137 changes: 137 additions & 0 deletions packages/common/src/dynamicModules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { DynamicModule, Provider, Type } from '@nestjs/common';
import { ModuleMetadata } from '@nestjs/common/interfaces';
import { get } from 'lodash';
import { interval, race, Subject } from 'rxjs';
import { first, map } from 'rxjs/operators';

type InjectionToken = string | symbol | Type<any>;

export interface ModuleConfigFactory<T> {
createModuleConfig(): Promise<T> | T;
}

export interface AsyncModuleConfig<T>
extends Pick<ModuleMetadata, 'imports' | 'exports'> {
useExisting?: {
value: ModuleConfigFactory<T>;
provide?: InjectionToken;
};
useClass?: Type<ModuleConfigFactory<T>>;
useFactory?: (...args: any[]) => Promise<T> | T;
inject?: any[];
}

export function createModuleConfigProvider<T>(
provide: InjectionToken,
options: AsyncModuleConfig<T>
): Provider {
if (options.useFactory) {
return {
provide,
useFactory: options.useFactory,
inject: options.inject || []
};
}

return {
provide,
useFactory: async (moduleConfigFactory: ModuleConfigFactory<T>) => {
const options = await moduleConfigFactory.createModuleConfig();
return options;
},
inject: [
options.useClass ||
get(
options,
'useExisting.provide',
(options.useExisting as any).value.constructor.name
)
]
};
}

export interface IConfigurableDynamicRootModule<T, U> {
new (): Type<T>;

moduleSubject: Subject<DynamicModule>;

forRoot(moduleCtor: Type<T>, moduleConfig: U): DynamicModule;

forRootAsync(
moduleCtor: Type<T>,
asyncModuleConfig: AsyncModuleConfig<U>
): DynamicModule;

externallyConfigured(
moduleCtor: Type<T>,
wait: number
): Promise<DynamicModule>;
}

export function MakeConfigurableDynamicRootModule<T, U>(
moduleConfigToken: InjectionToken,
additionalProviders: Provider[] = []
) {
abstract class DynamicRootModule {
static moduleSubject = new Subject<DynamicModule>();

static forRootAsync(
moduleCtor: Type<T>,
asyncModuleConfig: AsyncModuleConfig<U>
): DynamicModule {
const dynamicModule = {
module: moduleCtor,
imports: asyncModuleConfig.imports,
exports: asyncModuleConfig.exports,
providers: [
createModuleConfigProvider(moduleConfigToken, asyncModuleConfig),
...additionalProviders
]
};

DynamicRootModule.moduleSubject.next(dynamicModule);

return dynamicModule;
}

static forRoot(moduleCtor: Type<T>, moduleConfig: U): DynamicModule {
const dynamicModule = {
module: moduleCtor,
providers: [
{
provide: moduleConfigToken,
useValue: moduleConfig
},
...additionalProviders
]
};

DynamicRootModule.moduleSubject.next(dynamicModule);

return dynamicModule;
}

static async externallyConfigured(
moduleCtor: Type<T>,
wait: number
): Promise<DynamicModule> {
const timeout$ = interval(wait).pipe(
first(),
map(x => {
throw new Error(
`Expected ${
moduleCtor.name
} to be configured by at last one Module but it was not configured within ${wait}ms`
);
})
);

return race(
timeout$,
DynamicRootModule.moduleSubject.pipe(first())
).toPromise();
}
}

return DynamicRootModule as IConfigurableDynamicRootModule<T, U>;
}
1 change: 1 addition & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './mixins';
export * from './options';
export * from './dynamicModules';
4 changes: 1 addition & 3 deletions packages/common/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@ export interface OptionsFactory<T> {
createOptions(): Promise<T> | T;
}

// type OptionsFactoryImpl<T extends OptionsFactory<T>> = T;

export interface AsyncOptionsFactoryProvider<T>
extends Pick<ModuleMetadata, 'imports'> {
extends Pick<ModuleMetadata, 'imports' | 'exports'> {
useExisting?: {
value: OptionsFactory<T>;
provide?: string | symbol | Type<any>;
Expand Down
11 changes: 11 additions & 0 deletions packages/modules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# `@nestjs-plus/modules`

> TODO: description
## Usage

```
const modules = require('@nestjs-plus/modules');
// TODO: DEMONSTRATE API
```
32 changes: 32 additions & 0 deletions packages/modules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@nestjs-plus/modules",
"version": "0.0.1",
"description": "Badass building blocks for creating NestJS modules",
"keywords": [
"nestjs"
],
"author": "Jesse Carter <[email protected]>",
"homepage": "https://github.com/WonderPanda/nestjs-plus#readme",
"license": "MIT",
"main": "lib/modules.js",
"directories": {
"lib": "lib",
"test": "__tests__"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "git+https://github.com/WonderPanda/nestjs-plus.git"
},
"scripts": {
"test": "echo \"Error: run tests from root\" && exit 1"
},
"bugs": {
"url": "https://github.com/WonderPanda/nestjs-plus/issues"
}
}
2 changes: 1 addition & 1 deletion packages/rabbitmq/src/rabbitmq.constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const RABBIT_HANDLER = Symbol('RABBIT_HANDLER');
export const RABBIT_CONFIG = Symbol('RABBIT_CONFIG');
export const RABBIT_CONFIG_TOKEN = Symbol('RABBIT_CONFIG');
106 changes: 23 additions & 83 deletions packages/rabbitmq/src/rabbitmq.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,67 +8,44 @@ import {
} from '@nestjs/common';
import {
AsyncOptionsFactoryProvider,
createAsyncOptionsProvider
createAsyncOptionsProvider,
MakeConfigurableDynamicRootModule
} from '@nestjs-plus/common';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { groupBy } from 'lodash';
import { AmqpConnection } from './amqp/connection';
import { RABBIT_HANDLER, RABBIT_CONFIG } from './rabbitmq.constants';
import { RABBIT_HANDLER, RABBIT_CONFIG_TOKEN } from './rabbitmq.constants';
import { RabbitHandlerConfig, RabbitMQConfig } from './rabbitmq.interfaces';

@Module({
imports: [DiscoveryModule]
})
export class RabbitMQModule implements OnModuleInit {
export class RabbitMQModule
extends MakeConfigurableDynamicRootModule<RabbitMQModule, RabbitMQConfig>(
RABBIT_CONFIG_TOKEN,
[
{
provide: AmqpConnection,
useFactory: async (config: RabbitMQConfig): Promise<AmqpConnection> => {
const connection = new AmqpConnection(config);
await connection.init();
const logger = new Logger(RabbitMQModule.name);
logger.log('Successfully connected to RabbitMQ');
return connection;
},
inject: [RABBIT_CONFIG_TOKEN]
}
]
)
implements OnModuleInit {
private readonly logger = new Logger(RabbitMQModule.name);

constructor(
private readonly discover: DiscoveryService,
private readonly amqpConnection: AmqpConnection,
private readonly externalContextCreator: ExternalContextCreator
) {}

public static forRootAsync(
asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider<RabbitMQConfig>
): DynamicModule {
return {
module: RabbitMQModule,
exports: [AmqpConnection],
imports: asyncOptionsFactoryProvider.imports,
providers: [
...this.createAsyncProviders(asyncOptionsFactoryProvider),
{
provide: AmqpConnection,
useFactory: async (config): Promise<AmqpConnection> => {
const connection = new AmqpConnection(config);
await connection.init();
const logger = new Logger(RabbitMQModule.name);
logger.log('Successfully connected to RabbitMQ');
return connection;
},
inject: [RABBIT_CONFIG]
}
]
};
}

public static forRoot(config: RabbitMQConfig): DynamicModule {
return {
module: RabbitMQModule,
providers: [
{
provide: AmqpConnection,
useFactory: async (): Promise<AmqpConnection> => {
const connection = new AmqpConnection(config);
await connection.init();
const logger = new Logger(RabbitMQModule.name);
logger.log('Successfully connected to RabbitMQ');
return connection;
}
}
],
exports: [AmqpConnection]
};
) {
super();
}

public static build(config: RabbitMQConfig): DynamicModule {
Expand Down Expand Up @@ -146,41 +123,4 @@ export class RabbitMQModule implements OnModuleInit {
);
}
}

private static createAsyncProviders(
asyncOptionsFactoryProvider: AsyncOptionsFactoryProvider<RabbitMQConfig>
): Provider[] {
const optionsProvider = createAsyncOptionsProvider(
RABBIT_CONFIG,
asyncOptionsFactoryProvider
);

if (asyncOptionsFactoryProvider.useFactory) {
return [optionsProvider];
}

if (asyncOptionsFactoryProvider.useClass) {
return [
optionsProvider,
{
provide: asyncOptionsFactoryProvider.useClass,
useClass: asyncOptionsFactoryProvider.useClass
}
];
}

if (asyncOptionsFactoryProvider.useExisting) {
return [
optionsProvider,
{
provide:
asyncOptionsFactoryProvider.useExisting.provide ||
asyncOptionsFactoryProvider.useExisting.value.constructor.name,
useValue: asyncOptionsFactoryProvider.useExisting.value
}
];
}

return [];
}
}
Loading

0 comments on commit 80a2b2c

Please sign in to comment.