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(framework): Add NestJS serve handler #6654

Merged
merged 16 commits into from
Oct 9, 2024

Conversation

rifont
Copy link
Collaborator

@rifont rifont commented Oct 8, 2024

What changed? Why was the change needed?

  • Add idiomatic NestJS serve handler using NestJS modules and controllers
  • Add NestJS playground application to demonstrate best practice usage of the new handler

closes #6552

Screenshots

Basic DX (without NestJS dependency injection, only basic content Workflows using NovuModule.register):

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { workflow } from '@novu/framework';
import { NovuModule } from '@novu/framework/nest';

@Module({
  imports: [
    ConfigModule.forRoot({
      envFilePath: '.env',
    }),
    NovuModule.register({
      apiPath: '/api/novu',
      workflows: [
        workflow('welcome-workflow', async ({ step, payload }) => {
          await step.email('send-email', async (controls) => ({
            subject: `${controls.greeting}`,
            body: `We are glad you are here! UserId: ${payload.userId}`,
          }));
        }),
      ],
    }),
  ],
})
export class AppModule {}

Advanced DX (with NestJS dependency injection inside workflows using NovuModule.registerAsync):

// #############################################
// ########### notification.service.ts #########
// #############################################
// demonstrating declaration of workflows with access to injected services

import { Injectable } from '@nestjs/common';
import { workflow } from '@novu/framework';
import { z } from 'zod';
import { UserService } from './user.service';

@Injectable()
export class NotificationService {
  constructor(private readonly userService: UserService) {}

  public welcomeWorkflow() {
    return workflow(
      'welcome-workflow',
      async ({ step, payload }) => {
        await step.email(
          'send-email',
          async (controls) => {
            const user = this.userService.getUser(payload.userId);

            return {
              subject: `${controls.greeting}, ${user.name}`,
              body: `We are glad you are here! Email: ${user.email}`,
            };
          },
          {
            controlSchema: z.object({
              greeting: z.string().default('Welcome to our platform'),
            }),
          },
        );
      },
      {
        payloadSchema: z.object({
          userId: z.string(),
        }),
      },
    );
  }
}



// ############################################
// ############# app.module.ts ################
// ############################################
// demonstrating module initialization

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { NovuModule } from '@novu/framework/nest';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { NotificationService } from './notification.service';
import { UserService } from './user.service';

@Module({
  imports: [
    /*
     * IMPORTANT: ConfigModule must be imported before NovuModule to ensure
     * environment variables are loaded before the NovuModule is initialized.
     *
     * This ensures that NOVU_SECRET_KEY is available when the NovuModule is initialized.
     */
    ConfigModule.forRoot({
      envFilePath: '.env',
    }),
    NovuModule.registerAsync({
      imports: [AppModule],
      useFactory: (notificationService: NotificationService) => ({
        apiPath: '/api/novu',
        workflows: [notificationService.welcomeWorkflow()],
      }),
      inject: [NotificationService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService, NotificationService, UserService],
  exports: [NotificationService],
})
export class AppModule {}



// #############################################
// ############# app.controller.ts #############
// #############################################
// demonstrating type-safe `workflow().trigger` usage
import { Controller, Get, Param } from '@nestjs/common';
import { AppService } from './app.service';
import { NotificationService } from './notification.service';
import { UserService } from './user.service';

@Controller()
export class AppController {
  constructor(
    private readonly appService: AppService,
    private readonly notificationService: NotificationService,
    private readonly userService: UserService,
  ) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get('/welcome/:userId')
  public async sendWelcomeNotification(@Param('userId') userId: string) {
    const user = this.userService.getUser(userId);

    return this.notificationService.welcomeWorkflow().trigger({
      payload: { userId },
      to: {
        subscriberId: userId,
        email: user.email,
      },
    });
  }
}
Expand for optional sections

Related enterprise PR

Special notes for your reviewer

Copy link

linear bot commented Oct 8, 2024

import { ConfigurableModuleBuilder } from '@nestjs/common';
import { NovuModuleOptions } from './nest.interface';

// use ConfigurableModuleBuilder, because building dynamic modules from scratch is painful
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following best practices to use ConfigurableModuleBuilder per Dynamic Modules docs

Copy link

pkg-pr-new bot commented Oct 8, 2024

Open in Stackblitz

novu

pnpm add https://pkg.pr.new/novuhq/novu@6654

@novu/client

pnpm add https://pkg.pr.new/novuhq/novu/@novu/client@6654

@novu/framework

pnpm add https://pkg.pr.new/novuhq/novu/@novu/framework@6654

@novu/headless

pnpm add https://pkg.pr.new/novuhq/novu/@novu/headless@6654

@novu/js

pnpm add https://pkg.pr.new/novuhq/novu/@novu/js@6654

@novu/nest

pnpm add https://pkg.pr.new/novuhq/novu/@novu/nest@6654

@novu/notification-center

pnpm add https://pkg.pr.new/novuhq/novu/@novu/notification-center@6654

@novu/node

pnpm add https://pkg.pr.new/novuhq/novu/@novu/node@6654

@novu/providers

pnpm add https://pkg.pr.new/novuhq/novu/@novu/providers@6654

@novu/react

pnpm add https://pkg.pr.new/novuhq/novu/@novu/react@6654

@novu/react-native

pnpm add https://pkg.pr.new/novuhq/novu/@novu/react-native@6654

@novu/shared

pnpm add https://pkg.pr.new/novuhq/novu/@novu/shared@6654

@novu/stateless

pnpm add https://pkg.pr.new/novuhq/novu/@novu/stateless@6654

commit: 9b9d91b

Copy link

netlify bot commented Oct 8, 2024

Deploy Preview for novu-stg-vite-dashboard-poc ready!

Name Link
🔨 Latest commit 9b9d91b
🔍 Latest deploy log https://app.netlify.com/sites/novu-stg-vite-dashboard-poc/deploys/6706743dbe756900080f5d6b
😎 Deploy Preview https://deploy-preview-6654--novu-stg-vite-dashboard-poc.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@@ -13,6 +13,7 @@
"sourceMap": true,
"rootDir": ".",
"outDir": "./dist",
"experimentalDecorators": true,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Necessary to use NestJS decorators.

Copy link
Contributor

@SokratisVidros SokratisVidros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😍

@paulwer
Copy link
Contributor

paulwer commented Oct 8, 2024

While using the existing SDK without the distinct novu nestjs integration, we have leveraged the batch endpoint quite often to reduce the amount of outgoing requests. Therefore it would be awesome to have like Workflow Types which can be eigher reused within a batch function or a typesafe implementation choosing the right types based on the workflow name within the batch fn.

Besides this, this is a great feature and implementation, thanks for the work❤️

@rifont
Copy link
Collaborator Author

rifont commented Oct 9, 2024

While using the existing SDK without the distinct novu nestjs integration, we have leveraged the batch endpoint quite often to reduce the amount of outgoing requests. Therefore it would be awesome to have like Workflow Types which can be eigher reused within a batch function or a typesafe implementation choosing the right types based on the workflow name within the batch fn.

Besides this, this is a great feature and implementation, thanks for the work❤️

Thanks for sharing the use-case. We'll consider bulk type-safe triggering separately from the NestJS Framework handler, we will likely export a utility helper from @novu/framework rather than Web-Framework specific that does the following:

  1. Aggregates all the defined workflow(...) functions for usage in the serve handlers
  2. Performs Typescript generic manipulation of the defined workflow(...) functions with a discriminated union on the workflowId to derive the necessary types for triggering.

I'm raising this for consideration in our backlog 🙏

@rifont rifont merged commit 0e88116 into next Oct 9, 2024
40 checks passed
@rifont rifont deleted the nv-4183-introduce-nestjs-handler-for-novuframework branch October 9, 2024 13:14
@paulwer
Copy link
Contributor

paulwer commented Oct 9, 2024

Thanks for the clarification. Great progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

📚 Docs Feedback: Add informations about how to use Novu with NestJs
3 participants