Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: forRootAsync #83

Merged
merged 1 commit into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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