Skip to content

Commit

Permalink
feat(hasura): configurable controller prefix
Browse files Browse the repository at this point in the history
allows the user to configure the hasura controller prefix to a value of
their choosing. defaults to "haura"

fix #129
  • Loading branch information
WonderPanda committed Apr 5, 2020
1 parent 4adc481 commit 15c085c
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 53 deletions.
14 changes: 9 additions & 5 deletions packages/hasura/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
<img alt="license" src="https://img.shields.io/npm/l/@golevelup/nestjs-hasura.svg">
</p>

## Description
## Motivation

Exposes an api endpoint from your NestJS application at `/hasura/events/` to be used for event processing from Hasura. Provider methods decorated with `HasuraEventHandler` will be automatically wired up to handle incoming events received at the endpoint
Leverage NestJS to make incorporating business logic and event processing easier with Hasura. 🚀

## Motivation
## Features

Leverage NestJS to make incorporating business logic and event processing easier with Hasura.
- ✅ 🎉 Exposes an API endpoint from your NestJS application at to be used for event processing from Hasura. Defaults to `/hasura/events/` but can be easily configured
- ✅ 🔒 Automatically validates that the event payload was actually sent from Hasura using configurable secrets
- ✅ 🕵️ Discovers providers from your application decorated with `HasuraEventHandler` and routes incoming events to them
- ✅ 🧭 Leverage the table and schema name of the event to route to the appropriate handler

## Usage

Expand All @@ -36,6 +39,7 @@ import { HasuraModule } from '@golevelup/nestjs-hasura';
HasuraModule.forRoot(HasuraModule, {
secretFactory: secret,
secretHeader: secretHeader,
controllerPrefix: 'something', // this is optional. defaults to hasura
}),
],
})
Expand Down Expand Up @@ -83,7 +87,7 @@ https://hasura.io/event-triggers

#### Integrating with your NestJS app

The `HasuraModule` makes it easy to reuse the same events API endpoint for all events that you create in Hasura. The internal routing mechanism on the NestJS side ensures that the all events coming in through the endpoint will be sent to the correct handler. The endpoint provided is `/hasura/events`.
The `HasuraModule` makes it easy to reuse the same events API endpoint for all events that you create in Hasura. The internal routing mechanism on the NestJS side ensures that the all events coming in through the endpoint will be sent to the correct handler. The endpoint provided defaults to `/hasura/events`. This can be overriden with the module by specifying an alternative `controllerPrefix` so for example you could set this to `webhooks` and the resulting endpoint would be available at `/webhooks/events`.

#### Important!

Expand Down
3 changes: 2 additions & 1 deletion packages/hasura/src/hasura.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export interface HasuraEventHandlerConfig {
export interface HasuraModuleConfig {
secretHeader: string;
secretFactory: (() => string) | string;
enableLogs?: boolean;
enableEventLogs?: boolean;
controllerPrefix?: string;
}
21 changes: 19 additions & 2 deletions packages/hasura/src/hasura.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Module,
OnModuleInit,
} from '@nestjs/common';
import { PATH_METADATA } from '@nestjs/common/constants';
import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
import { flatten, groupBy } from 'lodash';
import { HASURA_EVENT_HANDLER, HASURA_MODULE_CONFIG } from './hasura.constants';
Expand All @@ -26,7 +27,23 @@ export class HasuraModule
extends createConfigurableDynamicRootModule<HasuraModule, HasuraModuleConfig>(
HASURA_MODULE_CONFIG,
{
providers: [EventHandlerService, HasuraEventHandlerHeaderGuard],
providers: [
{
provide: Symbol('CONTROLLER_HACK'),
useFactory: (config: HasuraModuleConfig) => {
const controllerPrefix = config.controllerPrefix || 'hasura';

Reflect.defineMetadata(
PATH_METADATA,
controllerPrefix,
EventHandlerController
);
},
inject: [HASURA_MODULE_CONFIG],
},
EventHandlerService,
HasuraEventHandlerHeaderGuard,
],
controllers: [EventHandlerController],
}
)
Expand Down Expand Up @@ -82,7 +99,7 @@ export class HasuraModule
// TODO: this should use a map for faster lookups
const handler = eventHandlers.find((x) => x.key === key);

if (this.hasuraModuleConfig.enableLogs) {
if (this.hasuraModuleConfig.enableEventLogs) {
this.logger.log(`Received event for: ${key}-${evt?.event?.op}`);
}

Expand Down
103 changes: 58 additions & 45 deletions packages/hasura/src/tests/hasura.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { HasuraEventHandler } from '../hasura.decorators';
import { HasuraModule } from '../hasura.module';

const eventHandlerFn = jest.fn();
const hasuraEndpoint = '/hasura/events';
const defaultHasuraEndpoint = '/hasura/events';

@Injectable()
class UserEventService {
Expand Down Expand Up @@ -38,53 +38,66 @@ const eventPayloadMissingTable = {
table: { schema: 'public', name: 'userz' },
};

describe('Hasura Module (e2e)', () => {
let app;
describe.each([undefined, 'customEndpoint'])(
'Hasura Module (e2e)',
(controllerPrefix) => {
let app;
const hasuraEndpoint = controllerPrefix
? `/${controllerPrefix}/events`
: defaultHasuraEndpoint;

beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
HasuraModule.forRoot(HasuraModule, {
secretFactory: secret,
secretHeader: secretHeader,
}),
],
providers: [UserEventService],
}).compile();
console.log(`HASURA ENDPOINT: ${hasuraEndpoint}`);

app = moduleFixture.createNestApplication();
await app.init();
});
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
HasuraModule.forRoot(HasuraModule, {
secretFactory: secret,
secretHeader: secretHeader,
controllerPrefix,
}),
],
providers: [UserEventService],
}).compile();

it('should return forbidden if the secret api header is missing', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.send(eventPayload)
.expect(403);
});
app = moduleFixture.createNestApplication();
await app.init();
});

it('should return forbidden if the secret api header value does not match', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, 'wrong Value')
.send(eventPayload)
.expect(403);
});
afterEach(() => {
eventHandlerFn.mockReset();
});

it('should return bad request if there is no event handler for the event', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, secret)
.send(eventPayloadMissingTable)
.expect(400);
});
it('should return forbidden if the secret api header is missing', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.send(eventPayload)
.expect(403);
});

it('should pass the event to the correct handler', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, secret)
.send(eventPayload)
.expect(202)
.then(() => expect(eventHandlerFn).toHaveBeenCalledTimes(1));
});
});
it('should return forbidden if the secret api header value does not match', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, 'wrong Value')
.send(eventPayload)
.expect(403);
});

it('should return bad request if there is no event handler for the event', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, secret)
.send(eventPayloadMissingTable)
.expect(400);
});

it('should pass the event to the correct handler', () => {
return request(app.getHttpServer())
.post(hasuraEndpoint)
.set(secretHeader, secret)
.send(eventPayload)
.expect(202)
.then(() => expect(eventHandlerFn).toHaveBeenCalledTimes(1));
});
}
);

0 comments on commit 15c085c

Please sign in to comment.