From 287f15b0b05d9ea3341ccf2351d1dcfaa2ede8a9 Mon Sep 17 00:00:00 2001 From: Sebastian Leidig Date: Wed, 14 Feb 2024 17:50:32 +0100 Subject: [PATCH] feat(notifications): add basic controllers for webhook endpoints --- src/notification/controller/dtos.ts | 14 +++ .../controller/webhook.controller.spec.ts | 18 ++++ .../controller/webhook.controller.ts | 98 +++++++++++++++++++ src/notification/core/notification.service.ts | 14 +++ src/notification/core/webhook.ts | 11 +++ src/notification/notification.module.ts | 5 +- .../storage/webhook-storage.service.spec.ts | 18 ++++ .../storage/webhook-storage.service.ts | 27 +++++ 8 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 src/notification/controller/dtos.ts create mode 100644 src/notification/controller/webhook.controller.spec.ts create mode 100644 src/notification/controller/webhook.controller.ts create mode 100644 src/notification/core/webhook.ts create mode 100644 src/notification/storage/webhook-storage.service.spec.ts create mode 100644 src/notification/storage/webhook-storage.service.ts diff --git a/src/notification/controller/dtos.ts b/src/notification/controller/dtos.ts new file mode 100644 index 0000000..35b39d2 --- /dev/null +++ b/src/notification/controller/dtos.ts @@ -0,0 +1,14 @@ +export interface WebhookDto {} + +export interface WebhookConfigurationDto { + name: string; + method: 'GET' | 'POST'; + targetUrl: string; + authenticationType: 'API_KEY'; + authentication: ApiKeyAuthConfig; +} + +export interface ApiKeyAuthConfig { + key: string; + headerName: string; +} diff --git a/src/notification/controller/webhook.controller.spec.ts b/src/notification/controller/webhook.controller.spec.ts new file mode 100644 index 0000000..959fe61 --- /dev/null +++ b/src/notification/controller/webhook.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WebhookController } from './webhook.controller'; + +describe('WebhookController', () => { + let controller: WebhookController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [WebhookController], + }).compile(); + + controller = module.get(WebhookController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/notification/controller/webhook.controller.ts b/src/notification/controller/webhook.controller.ts new file mode 100644 index 0000000..4a720cd --- /dev/null +++ b/src/notification/controller/webhook.controller.ts @@ -0,0 +1,98 @@ +import { + Body, + Controller, + Delete, + Get, + Headers, + Param, + Post, +} from '@nestjs/common'; +import { defaultIfEmpty, map, Observable, zipAll } from 'rxjs'; +import { Reference } from '../../domain/reference'; +import { WebhookStorageService } from '../storage/webhook-storage.service'; +import { Webhook } from '../core/webhook'; +import { NotificationService } from '../core/notification.service'; +import { WebhookConfigurationDto, WebhookDto } from './dtos'; + +@Controller('/api/v1/notifications/webhook') +export class WebhookController { + constructor( + private webhookStorage: WebhookStorageService, + private notificationService: NotificationService, + ) {} + + @Get() + fetchWebhooksOfUser( + @Headers('Authorization') token: string, + ): Observable { + return this.webhookStorage.fetchAllWebhooks(token).pipe( + map((webhooks) => webhooks.map((webhook) => this.getWebhookDto(webhook))), + zipAll(), + defaultIfEmpty([]), + ); + } + + @Get('/:webhookId') + fetchWebhook( + @Headers('Authorization') token: string, + @Param('webhookId') webhookId: string, + ): Observable { + return this.webhookStorage.fetchWebhook(new Reference(webhookId)).pipe( + // TODO: check auth? + // TODO: map to 404 if undefined + map((webhook) => this.getWebhookDto(webhook as any)), + ); + } + + @Post() + createWebhook( + @Headers('Authorization') token: string, + @Body() requestBody: WebhookConfigurationDto, + ): Observable { + return this.webhookStorage.createWebhook(requestBody).pipe( + // TODO: check auth? + // TODO: map errors to response codes + map((webhookRef: Reference) => webhookRef.id), + ); + } + + @Post('/:webhookId/subscribe/report/:reportId') + subscribeReportNotifications( + @Headers('Authorization') token: string, + @Param('webhookId') webhookId: string, + @Param('reportId') reportId: string, + ): Observable { + return this.notificationService + .registerForReportEvents( + new Reference(webhookId), + new Reference(reportId), + ) + .pipe + // TODO: check auth? + // TODO: map errors to response codes + // TODO: map to 200 Response without body (otherwise service throws error) + (); + } + + @Delete('/:webhookId/subscribe/report/:reportId') + unsubscribeReportNotifications( + @Headers('Authorization') token: string, + @Param('webhookId') webhookId: string, + @Param('reportId') reportId: string, + ): Observable { + return this.notificationService + .unregisterForReportEvents( + new Reference(webhookId), + new Reference(reportId), + ) + .pipe + // TODO: check auth? + // TODO: map errors to response codes + // TODO: map to 200 Response without body (otherwise service throws error) + (); + } + + private getWebhookDto(webhook: Webhook): WebhookDto { + return webhook; + } +} diff --git a/src/notification/core/notification.service.ts b/src/notification/core/notification.service.ts index d5b2a17..4a852d7 100644 --- a/src/notification/core/notification.service.ts +++ b/src/notification/core/notification.service.ts @@ -22,4 +22,18 @@ export class NotificationService { * Trigger a core event for the given report to any active subscribers. */ triggerNotification(event: ReportDataChangeEvent): void {} + + registerForReportEvents( + webhook: Reference, + report: Reference, + ): Observable { + return of(); + } + + unregisterForReportEvents( + webhook: Reference, + report: Reference, + ): Observable { + return of(); + } } diff --git a/src/notification/core/webhook.ts b/src/notification/core/webhook.ts new file mode 100644 index 0000000..2fc384d --- /dev/null +++ b/src/notification/core/webhook.ts @@ -0,0 +1,11 @@ +import { Reference } from '../../domain/reference'; +import { WebhookConfigurationDto } from '../controller/dtos'; + +export interface Webhook extends WebhookConfiguration { + id: string; + name: string; // TODO: why name? + + reportSubscriptions: Reference[]; +} + +export type WebhookConfiguration = WebhookConfigurationDto; diff --git a/src/notification/notification.module.ts b/src/notification/notification.module.ts index bafb43b..630a972 100644 --- a/src/notification/notification.module.ts +++ b/src/notification/notification.module.ts @@ -1,8 +1,11 @@ import { Module } from '@nestjs/common'; import { NotificationService } from './core/notification.service'; +import { WebhookStorageService } from './storage/webhook-storage.service'; +import { WebhookController } from './controller/webhook.controller'; @Module({ - providers: [NotificationService], + controllers: [WebhookController], + providers: [NotificationService, WebhookStorageService], exports: [NotificationService], }) export class NotificationModule {} diff --git a/src/notification/storage/webhook-storage.service.spec.ts b/src/notification/storage/webhook-storage.service.spec.ts new file mode 100644 index 0000000..4478c71 --- /dev/null +++ b/src/notification/storage/webhook-storage.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { WebhookStorageService } from './webhook-storage.service'; + +describe('WebhookStorageService', () => { + let service: WebhookStorageService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [WebhookStorageService], + }).compile(); + + service = module.get(WebhookStorageService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/notification/storage/webhook-storage.service.ts b/src/notification/storage/webhook-storage.service.ts new file mode 100644 index 0000000..9b35b50 --- /dev/null +++ b/src/notification/storage/webhook-storage.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@nestjs/common'; +import { Observable, of } from 'rxjs'; +import { Webhook, WebhookConfiguration } from '../core/webhook'; +import { Reference } from '../../domain/reference'; + +@Injectable() +export class WebhookStorageService { + /** + * Get all registered webhooks subscribe by the user authenticated with the given token + * @param token + */ + fetchAllWebhooks(token: string): Observable { + return of([]); + } + + fetchWebhook(webhook: Reference): Observable { + return of(undefined); + } + + /** + * Creates a new webhook with the given configuration, stores it and returns a reference to the new webhook. + * @param webhookConfig + */ + createWebhook(webhookConfig: WebhookConfiguration): Observable { + return of(new Reference('new-webhook-id')); + } +}