Skip to content

Commit

Permalink
feat: forRootAsync (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
simenandre authored Dec 31, 2021
1 parent 0b662ab commit 5c06158
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 109 deletions.
3 changes: 2 additions & 1 deletion src/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Test } from '@nestjs/testing';
import { SlackConfig, SlackModule } from '../slack.module';
import { SlackModule } from '../slack.module';
import { SlackConfig } from '../types';

export const createApp = (options?: Partial<SlackConfig>) => {
return Test.createTestingModule({
Expand Down
78 changes: 78 additions & 0 deletions src/__tests__/module.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Test } from '@nestjs/testing';
import { SlackModule } from '../slack.module';
import { SlackService } from '../slack.service';
import * as nock from 'nock';
import { Inject, Injectable, Module } from '@nestjs/common';
import { SlackConfig } from '../types';

interface Config {
slackWebhookUrl: string;
}

describe('slack.module', () => {
const baseUrl = 'http://example.com';

it('should construct with useFactory', async () => {
const app = await Test.createTestingModule({
imports: [
SlackModule.forRootAsync({
useFactory: () => {
return {
type: 'webhook',
webhookOptions: { url: `${baseUrl}/webhook` },
};
},
}),
],
}).compile();
const service = app.get<SlackService>(SlackService);

const scope = nock(baseUrl, { encodedQueryParams: true })
.post('/webhook', {
text: 'hello-world',
})
.reply(200, 'ok');

await service.postMessage({ text: 'hello-world' });

scope.done();
});

it('should construct with useClass', async () => {
@Injectable()
class ConfigClass {
slackConfigModuleOptions(): SlackConfig {
return {
type: 'webhook',
webhookOptions: { url: `${baseUrl}/webhook` },
};
}
}

@Module({
exports: [ConfigClass],
providers: [ConfigClass],
})
class TestModule {}

const app = await Test.createTestingModule({
imports: [
SlackModule.forRootAsync({
imports: [TestModule],
useClass: ConfigClass,
}),
],
}).compile();
const service = app.get<SlackService>(SlackService);

const scope = nock(baseUrl, { encodedQueryParams: true })
.post('/webhook', {
text: 'hello-world',
})
.reply(200, 'ok');

await service.postMessage({ text: 'hello-world' });

scope.done();
});
});
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const SLACK_MODULE_OPTIONS = 'SlackModuleOptions';
export const SLACK_MODULE_USER_OPTIONS = 'SlackModuleUserOptions';
export const SLACK_WEB_CLIENT = 'SlackWebClient';
export const SLACK_WEBHOOK_URL = 'SlackWebhookUrl';
export const GOOGLE_LOGGING = 'SlackGoogleLogging';
194 changes: 87 additions & 107 deletions src/slack.module.ts
Original file line number Diff line number Diff line change
@@ -1,145 +1,125 @@
import { DynamicModule, Module, Provider } from '@nestjs/common';
import type { WebClientOptions } from '@slack/web-api';
import {
GOOGLE_LOGGING,
SLACK_MODULE_OPTIONS,
SLACK_MODULE_USER_OPTIONS,
SLACK_WEBHOOK_URL,
SLACK_WEB_CLIENT,
} from './constants';
import { SlackService } from './slack.service';
import {
SlackAsyncConfig,
SlackConfig,
SlackConfigFactory,
SlackSyncConfig,
} from './types';
import { invariant } from './utils';

export interface SlackApiOptions {
/**
* You'll need a token to authenticate with Slack Web API
* Read more: https://api.slack.com/tutorials/tracks/getting-a-token
*/
token: string;

clientOptions?: WebClientOptions;
}

export interface SlackWebhookOptions {
/**
* Incoming Webhooks are a simple way to post messages from apps into Slack.
* Creating an Incoming Webhook gives you a unique URL to which you send a
* JSON payload with the message text and some options.
*
* Read more: https://api.slack.com/messaging/webhooks
*/
url: string;
}

export type SlackRequestType = 'api' | 'webhook' | 'stdout' | 'google';

export interface SlackConfig {
/**
* This argument refers to how you want to send requests
* to Slack.
*
* `api` is the default option, it utilizes `@slack/web-api`, which also
* requires setting `apiOptions`. Setting `stdout` and `google` makes
* this module send requests directly to stdout as a JSON-string. This is
* useful where you're consuming logs and want to forward them to Slack.
* `google` provides a JSON structure as Google Logging wants.
*
* **Note**: We suggest using a distributed model where logs are consumed
* when logging to Slack in production; it's easier to dump something to
* to a logger than calling the Slack Web API.
*
* @default stdout
*/
type: SlackRequestType;

/**
* This option is used when channel isn't defined
* when sending a request.
*/
defaultChannel?: string;

/**
* These configuration options are only required when type is set to
* `api`.
*/
apiOptions?: SlackApiOptions;

/**
* These configuration options are only required when type is set to
* `api`.
*/
webhookOptions?: SlackWebhookOptions;

/**
* Setting this changes which function is used to stdout.
*
* Only used for types `stdout`
*/
output?: (out: unknown) => void;

// If true, registers `SlackModule` as a global module.
isGlobal?: boolean;
}

@Module({
providers: [SlackService],
exports: [SlackService],
})
export class SlackModule {
static forRoot(opts?: Partial<SlackConfig>): DynamicModule {
const options: SlackConfig = {
type: 'stdout',
output: /* istanbul ignore next */ (out: unknown) =>
process.stdout.write(`${JSON.stringify(out)}\n`),
...opts,
};

static forRoot(opts: Partial<SlackSyncConfig> = {}): DynamicModule {
const providers = [
{
provide: SLACK_MODULE_OPTIONS,
useValue: options,
provide: SLACK_MODULE_USER_OPTIONS,
useValue: opts,
},
this.createAsyncGoogleLogger(options),
this.createAsyncWebClient(options),
this.createAsyncWebhook(options),
this.createAsyncConfig(),
this.createAsyncGoogleLogger(),
this.createAsyncWebClient(),
this.createAsyncWebhook(),
];
return {
global: options.isGlobal,
global: opts.isGlobal,
module: SlackModule,
providers,
exports: providers,
};
}

private static createAsyncGoogleLogger({ type }: SlackConfig): Provider {
if (type !== 'google') {
static forRootAsync(opts: SlackAsyncConfig): DynamicModule {
const providers = [
this.createAsyncOptionsProvider(opts),
this.createAsyncConfig(),
this.createAsyncGoogleLogger(),
this.createAsyncWebClient(),
this.createAsyncWebhook(),
];
return {
global: opts.isGlobal,
module: SlackModule,
imports: opts.imports,
providers,
exports: providers,
};
}

private static createAsyncOptionsProvider(opts: SlackAsyncConfig): Provider {
if (opts.useFactory) {
return {
provide: GOOGLE_LOGGING,
useValue: null,
provide: SLACK_MODULE_USER_OPTIONS,
useFactory: opts.useFactory,
inject: opts.inject || [],
};
}
invariant(opts.useClass);
return {
provide: SLACK_MODULE_USER_OPTIONS,
useFactory: async (
optionsFactory: SlackConfigFactory,
): Promise<SlackConfig> => optionsFactory.slackConfigModuleOptions(),
inject: [opts.useClass],
};
}

private static createAsyncConfig(): Provider {
return {
provide: SLACK_MODULE_OPTIONS,
inject: [SLACK_MODULE_USER_OPTIONS],
useFactory: async (opts: SlackConfig) => {
return {
type: 'stdout',
output: /* istanbul ignore next */ (out: unknown) =>
process.stdout.write(`${JSON.stringify(out)}\n`),
...opts,
};
},
};
}

private static createAsyncGoogleLogger(): Provider {
return {
provide: GOOGLE_LOGGING,
useFactory: async () => {
inject: [SLACK_MODULE_OPTIONS],
useFactory: async (opts: SlackConfig) => {
if (opts.type !== 'google') {
return {
provide: GOOGLE_LOGGING,
useValue: null,
};
}

const { Logging } = await import('@google-cloud/logging');
const logging = new Logging();
return logging.logSync('slack');
},
};
}

private static createAsyncWebClient({ type }: SlackConfig): Provider {
if (type !== 'api') {
return {
provide: SLACK_WEB_CLIENT,
useValue: null,
};
}

private static createAsyncWebClient(): Provider {
return {
provide: SLACK_WEB_CLIENT,
inject: [SLACK_MODULE_OPTIONS],
useFactory: async (opts: SlackConfig) => {
if (opts.type !== 'api') {
return {
provide: SLACK_WEB_CLIENT,
useValue: null,
};
}

invariant(
opts.apiOptions,
'You must provide `apiOptions` when using the api type.',
Expand All @@ -154,18 +134,18 @@ export class SlackModule {
};
}

private static createAsyncWebhook({ type }: SlackConfig): Provider {
if (type !== 'webhook') {
return {
provide: SLACK_WEBHOOK_URL,
useValue: null,
};
}

private static createAsyncWebhook(): Provider {
return {
provide: SLACK_WEBHOOK_URL,
inject: [SLACK_MODULE_OPTIONS],
useFactory: async (opts: SlackConfig) => {
if (opts.type !== 'webhook') {
return {
provide: SLACK_WEBHOOK_URL,
useValue: null,
};
}

invariant(
opts.webhookOptions,
'You must provide `webhookOptions` when using the webhook type.',
Expand Down
2 changes: 1 addition & 1 deletion src/slack.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
SLACK_WEBHOOK_URL,
SLACK_WEB_CLIENT,
} from './constants';
import type { SlackConfig, SlackRequestType } from './slack.module';
import type { SlackConfig, SlackRequestType } from './types';
import { invariant } from './utils';

export type SlackMessageOptions = Partial<ChatPostMessageArguments>;
Expand Down
Loading

0 comments on commit 5c06158

Please sign in to comment.