Skip to content

Commit

Permalink
Merge pull request #8 from chvarkov/develop
Browse files Browse the repository at this point in the history
Async module initialization
  • Loading branch information
chvarkov authored Oct 28, 2020
2 parents f68ed45 + be3ccc8 commit 9c90354
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 6 deletions.
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,22 @@ export class AppModule {
}
```

If you want import configs from your [ConfigService](https://docs.nestjs.com/techniques/configuration#getting-started) via [custom getter function](https://docs.nestjs.com/techniques/configuration#custom-getter-functions) that will return `GoogleRecaptchaModuleOptions` object.

```typescript
@Module({
imports: [
GoogleRecaptchaModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => configService.googleRecaptchaOptions,
inject: [ConfigService],
})
],
})
export class AppModule {
}
```

### Usage <a name="Usage"></a>

Use `@Recaptcha` decorator to protect your endpoints.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestlab/google-recaptcha",
"version": "1.1.2",
"version": "1.1.3",
"description": "Google recaptcha module for NestJS.",
"keywords": [
"nest",
Expand Down
66 changes: 62 additions & 4 deletions src/google-recaptcha.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { DynamicModule, HttpModule, Module, Provider } from '@nestjs/common';
import { GoogleRecaptchaGuard } from './guards/google-recaptcha.guard';
import { GoogleRecaptchaValidator } from './services/google-recaptcha.validator';
import { GoogleRecaptchaModuleOptions } from './interfaces/google-recaptcha-module-options';
import {
GoogleRecaptchaModuleAsyncOptions,
GoogleRecaptchaModuleOptions, GoogleRecaptchaOptionsFactory
} from './interfaces/google-recaptcha-module-options';
import { RECAPTCHA_OPTIONS } from './provider.declarations';

@Module({
})
@Module({})
export class GoogleRecaptchaModule {
static forRoot(options: GoogleRecaptchaModuleOptions): DynamicModule {
const providers: Provider[] = [
Expand All @@ -17,13 +19,69 @@ export class GoogleRecaptchaModule {
},
];

return {
module: GoogleRecaptchaModule,
imports: [
HttpModule
],
providers: providers,
exports: providers,
}
}

static forRootAsync(options: GoogleRecaptchaModuleAsyncOptions): DynamicModule {
const providers: Provider[] = [
GoogleRecaptchaGuard,
GoogleRecaptchaValidator,
...this.createAsyncProviders(options)
];

return {
module: GoogleRecaptchaModule,
imports: [
HttpModule,
...options.imports || []
],
providers,
providers: providers,
exports: providers,
}
}

private static createAsyncProviders(options: GoogleRecaptchaModuleAsyncOptions): Provider[] {
const providers: Provider[] = [this.createAsyncOptionsProvider(options)];

if (options.useClass) {
providers.push({
provide: options.useClass,
useClass: options.useClass,
});
}

return providers;
}

private static createAsyncOptionsProvider(options: GoogleRecaptchaModuleAsyncOptions): Provider {
if (options.useFactory) {
return {
provide: RECAPTCHA_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}

return {
provide: RECAPTCHA_OPTIONS,
useFactory: (optionsFactory: GoogleRecaptchaOptionsFactory) => {
if (!this.isGoogleRecaptchaFactory(optionsFactory)) {
throw new Error('Factory must be implement \'GoogleRecaptchaOptionsFactory\' interface.')
}
return optionsFactory.createGoogleRecaptchaOptions();
},
inject: [options.useExisting! || options.useClass!],
};
}

private static isGoogleRecaptchaFactory(object: any): object is GoogleRecaptchaOptionsFactory {
return !!object && typeof object.createGoogleRecaptchaOptions === 'function';
}
}
13 changes: 12 additions & 1 deletion src/interfaces/google-recaptcha-module-options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { GoogleRecaptchaGuardOptions } from './google-recaptcha-guard-options';
import { GoogleRecaptchaValidatorOptions } from './google-recaptcha-validator-options';
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';

export interface GoogleRecaptchaModuleOptions extends GoogleRecaptchaValidatorOptions, GoogleRecaptchaGuardOptions {
export interface GoogleRecaptchaModuleOptions extends GoogleRecaptchaValidatorOptions, GoogleRecaptchaGuardOptions {}

export interface GoogleRecaptchaOptionsFactory {
createGoogleRecaptchaOptions(): Promise<GoogleRecaptchaModuleOptions> | GoogleRecaptchaModuleOptions;
}

export interface GoogleRecaptchaModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
inject?: any[];
useClass?: Type<GoogleRecaptchaOptionsFactory>;
useExisting?: Type<GoogleRecaptchaOptionsFactory>;
useFactory?: (...args: any[]) => Promise<GoogleRecaptchaModuleOptions> | GoogleRecaptchaModuleOptions;
}
102 changes: 102 additions & 0 deletions test/google-recaptcha-async-module.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Test } from '@nestjs/testing';
import { Module } from '@nestjs/common';
import { GoogleRecaptchaValidator } from '../src/services/google-recaptcha.validator';
import { GoogleRecaptchaModule } from '../src/google-recaptcha.module';
import { GoogleRecaptchaModuleOptions } from '../src';
import { GoogleRecaptchaOptionsFactory } from '../src/interfaces/google-recaptcha-module-options';

export class GoogleRecaptchaModuleOptionsFactory implements GoogleRecaptchaOptionsFactory {
createGoogleRecaptchaOptions(): Promise<GoogleRecaptchaModuleOptions> {
return Promise.resolve(new TestConfigService().getGoogleRecaptchaOptions());
}
}

export class TestConfigService {
getGoogleRecaptchaOptions(): GoogleRecaptchaModuleOptions {
return {
secretKey: 'secret',
response: req => req.body.recaptcha,
skipIf: () => true,
};
}
}

@Module({
providers: [
TestConfigService,
GoogleRecaptchaModuleOptionsFactory,
],
exports: [
TestConfigService,
GoogleRecaptchaModuleOptionsFactory,
],
})
export class TestConfigModule {

}

describe('Google recaptcha async module', () => {
test('Test via import module', async () => {
const testingModule = await Test.createTestingModule({
imports: [
GoogleRecaptchaModule.forRootAsync({
imports: [TestConfigModule],
useFactory: (config: TestConfigService) => config.getGoogleRecaptchaOptions(),
inject: [
TestConfigService,
],
}),
],
}).compile();

const app = testingModule.createNestApplication();

const validator = app.get(GoogleRecaptchaValidator);
expect(validator).toBeInstanceOf(GoogleRecaptchaValidator);
});

test('Test via useClass', async () => {
const testingModule = await Test.createTestingModule({
imports: [
GoogleRecaptchaModule.forRootAsync({
useClass: GoogleRecaptchaModuleOptionsFactory,
}),
],
}).compile();

const app = testingModule.createNestApplication();

const validator = app.get(GoogleRecaptchaValidator);
expect(validator).toBeInstanceOf(GoogleRecaptchaValidator);
});

test('Test via useExisting', async () => {
const testingModule = await Test.createTestingModule({
imports: [
GoogleRecaptchaModule.forRootAsync({
imports: [
TestConfigModule,
],
useExisting: GoogleRecaptchaModuleOptionsFactory,
}),
],
}).compile();

const app = testingModule.createNestApplication();

const validator = app.get(GoogleRecaptchaValidator);
expect(validator).toBeInstanceOf(GoogleRecaptchaValidator);
});

test('Test via useClass that not implement GoogleRecaptchaOptionsFactory', async () => {
await Test.createTestingModule({
imports: [
GoogleRecaptchaModule.forRootAsync({
useClass: TestConfigModule as any,
}),
],
}).compile()
.then(() => expect(true).toBeFalsy())
.catch(e => expect(e.message).toBe('Factory must be implement \'GoogleRecaptchaOptionsFactory\' interface.'));
});
});

0 comments on commit 9c90354

Please sign in to comment.