-
Notifications
You must be signed in to change notification settings - Fork 3.9k
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
rifont
merged 16 commits into
next
from
nv-4183-introduce-nestjs-handler-for-novuframework
Oct 9, 2024
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
45d0fa8
feat(nest): add NestJS support to framework
rifont b7995f2
refactor(controller): rename and add request handlers
rifont fa38d3f
Merge branch 'next' into nv-4183-introduce-nestjs-handler-for-novufra…
rifont 80e7d3c
feat(nest): add NovuBaseModule for dynamic modules
rifont cb1c0eb
refactor(nest): simplify module registration logic
rifont d481de5
feat(nestjs): Add NestJS playground application
rifont afe8faf
feat(nestjs): add welcome notification endpoint
rifont dbd5081
Update packages/framework/src/handler.ts
rifont cae0f25
refactor(nest): rename nest.decorator to register-api-path
rifont 61dc860
Merge branch 'nv-4183-introduce-nestjs-handler-for-novuframework' of …
rifont ebf8480
docs(nest.module): add async registration doc comment
rifont fb8f6ba
refactor(nest): reorder and add exports in nest.ts
rifont 575939b
chore: update cspell words list
rifont 179fb02
refactor(app.module): rename service variable
rifont 33a6777
style: remove unnecessary commas in code
rifont 9b9d91b
refactor(nest): replace NovuService with NovuClient
rifont File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -651,6 +651,7 @@ | |
"unarchived", | ||
"Unarchived", | ||
"Unfetch", | ||
"unplugin", | ||
"Unpromoted", | ||
"unpublish", | ||
"unsub", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
export * from './nest/nest.constants'; | ||
export * from './nest/nest.controller'; | ||
export * from './nest/nest.interface'; | ||
export * from './nest/nest.module'; | ||
export * from './nest/nest.register-api-path'; | ||
export * from './nest/nest.client'; | ||
export * from './nest/nest.handler'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { Injectable, Inject } from '@nestjs/common'; | ||
import type { Request, Response } from 'express'; | ||
|
||
import { NovuRequestHandler, type ServeHandlerOptions } from '../../handler'; | ||
import type { SupportedFrameworkName } from '../../types'; | ||
import { NOVU_OPTIONS } from './nest.constants'; | ||
import { NovuHandler } from './nest.handler'; | ||
|
||
export const frameworkName: SupportedFrameworkName = 'nest'; | ||
|
||
@Injectable() | ||
export class NovuClient { | ||
public novuRequestHandler: NovuRequestHandler; | ||
|
||
constructor( | ||
@Inject(NOVU_OPTIONS) private options: ServeHandlerOptions, | ||
@Inject(NovuHandler) private novuHandler: NovuHandler | ||
) { | ||
this.novuRequestHandler = new NovuRequestHandler({ | ||
frameworkName, | ||
...this.options, | ||
handler: this.novuHandler.handler, | ||
}); | ||
} | ||
|
||
public async handleRequest(req: Request, res: Response) { | ||
await this.novuRequestHandler.createHandler()(req, res); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const REGISTER_API_PATH = 'REGISTER_API_PATH'; | ||
export { NOVU_OPTIONS } from './nest.module-definition'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import { Controller, Req, Res, Inject, Get, Post, Options } from '@nestjs/common'; | ||
import { Request, Response } from 'express'; | ||
import { NovuClient } from './nest.client'; | ||
|
||
@Controller() | ||
export class NovuController { | ||
constructor(@Inject(NovuClient) private novuService: NovuClient) {} | ||
|
||
@Get() | ||
async handleGet(@Req() req: Request, @Res() res: Response) { | ||
await this.novuService.handleRequest(req, res); | ||
} | ||
|
||
@Post() | ||
async handlePost(@Req() req: Request, @Res() res: Response) { | ||
await this.novuService.handleRequest(req, res); | ||
} | ||
|
||
@Options() | ||
async handleOptions(@Req() req: Request, @Res() res: Response) { | ||
await this.novuService.handleRequest(req, res); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { type VercelRequest, type VercelResponse } from '@vercel/node'; | ||
import { Injectable } from '@nestjs/common'; | ||
import type { Request, Response } from 'express'; | ||
|
||
import { type INovuRequestHandlerOptions } from '../../handler'; | ||
import type { Either } from '../../types'; | ||
|
||
@Injectable() | ||
export class NovuHandler { | ||
public handler( | ||
incomingRequest: Either<VercelRequest, Request>, | ||
response: Either<Response, VercelResponse> | ||
): ReturnType<INovuRequestHandlerOptions['handler']> { | ||
const extractHeader = (key: string): string | null | undefined => { | ||
const header = incomingRequest.headers[key.toLowerCase()]; | ||
|
||
return Array.isArray(header) ? header[0] : header; | ||
}; | ||
|
||
return { | ||
body: () => incomingRequest.body, | ||
headers: extractHeader, | ||
method: () => incomingRequest.method || 'GET', | ||
queryString: (key) => { | ||
const qs = incomingRequest.query[key]; | ||
|
||
return Array.isArray(qs) ? qs[0] : qs; | ||
}, | ||
url: () => { | ||
// `req.hostname` can filter out port numbers; beware! | ||
const hostname = incomingRequest.headers.host || ''; | ||
|
||
const protocol = hostname?.includes('://') ? '' : `${incomingRequest.protocol || 'https'}://`; | ||
|
||
const url = new URL(incomingRequest.originalUrl || incomingRequest.url || '', `${protocol}${hostname || ''}`); | ||
|
||
return url; | ||
}, | ||
transformResponse: ({ body, headers, status }) => { | ||
Object.entries(headers).forEach(([headerName, headerValue]) => { | ||
response.setHeader(headerName, headerValue as string); | ||
}); | ||
|
||
return response.status(status).send(body); | ||
}, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import type { ServeHandlerOptions } from '../../handler'; | ||
|
||
export type NovuModuleOptions = ServeHandlerOptions & { | ||
apiPath: string; | ||
}; |
17 changes: 17 additions & 0 deletions
17
packages/framework/src/servers/nest/nest.module-definition.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { ConfigurableModuleBuilder } from '@nestjs/common'; | ||
import { NovuModuleOptions } from './nest.interface'; | ||
|
||
// use ConfigurableModuleBuilder, because building dynamic modules from scratch is painful | ||
export const { | ||
ConfigurableModuleClass: NovuBaseModule, | ||
MODULE_OPTIONS_TOKEN: NOVU_OPTIONS, | ||
OPTIONS_TYPE, | ||
ASYNC_OPTIONS_TYPE, | ||
} = new ConfigurableModuleBuilder<NovuModuleOptions>() | ||
.setClassMethodName('register') | ||
.setFactoryMethodName('createNovuModuleOptions') | ||
.setExtras((definition: NovuModuleOptions) => ({ | ||
...definition, | ||
isGlobal: true, | ||
})) | ||
.build(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
import { Module, Provider } from '@nestjs/common'; | ||
import { NovuClient } from './nest.client'; | ||
import { NovuController } from './nest.controller'; | ||
import { registerApiPath } from './nest.register-api-path'; | ||
import { ASYNC_OPTIONS_TYPE, NovuBaseModule, OPTIONS_TYPE } from './nest.module-definition'; | ||
import { NovuHandler } from './nest.handler'; | ||
|
||
/** | ||
* In NestJS, serve and register any declared workflows with Novu, making | ||
* them available to be triggered by events. | ||
* | ||
* @example | ||
* ```ts | ||
* import { NovuModule } from "@novu/framework/nest"; | ||
* import { myWorkflow } from "./src/novu/workflows"; // Your workflows | ||
* | ||
* @Module({ | ||
* imports: [ | ||
* // Expose the middleware on our recommended path at `/api/novu`. | ||
* NovuModule.register({ | ||
* apiPath: '/api/novu', | ||
* workflows: [myWorkflow] | ||
* }) | ||
* ] | ||
* }) | ||
* export class AppModule {} | ||
* | ||
* const app = await NestFactory.create(AppModule); | ||
* | ||
* // Important: ensure you add JSON middleware to process incoming JSON POST payloads. | ||
* app.use(express.json()); | ||
* ``` | ||
*/ | ||
@Module({}) | ||
export class NovuModule extends NovuBaseModule { | ||
/** | ||
* Register the Novu module | ||
* | ||
* @param options - The options to register the Novu module | ||
* @param customProviders - Custom providers to register. These will be merged with the default providers. | ||
* @returns The Novu module | ||
*/ | ||
static register(options: typeof OPTIONS_TYPE, customProviders?: Provider[]) { | ||
const superModule = super.register(options); | ||
|
||
superModule.controllers = [NovuController]; | ||
superModule.providers?.push(registerApiPath, NovuClient, NovuHandler, ...(customProviders || [])); | ||
superModule.exports = [NovuClient, NovuHandler]; | ||
|
||
return superModule; | ||
} | ||
|
||
/** | ||
* Register the Novu module asynchronously | ||
* | ||
* @param options - The options to register the Novu module | ||
* @param customProviders - Custom providers to register. These will be merged with the default providers. | ||
* @returns The Novu module | ||
*/ | ||
static registerAsync(options: typeof ASYNC_OPTIONS_TYPE, customProviders?: Provider[]) { | ||
const superModule = super.registerAsync(options); | ||
|
||
superModule.controllers = [NovuController]; | ||
superModule.providers?.push(registerApiPath, NovuClient, NovuHandler, ...(customProviders || [])); | ||
superModule.exports = [NovuClient, NovuHandler]; | ||
|
||
return superModule; | ||
} | ||
} |
26 changes: 26 additions & 0 deletions
26
packages/framework/src/servers/nest/nest.register-api-path.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { FactoryProvider } from '@nestjs/common'; | ||
import { PATH_METADATA } from '@nestjs/common/constants'; | ||
import { NovuController } from './nest.controller'; | ||
import { REGISTER_API_PATH, NOVU_OPTIONS } from './nest.constants'; | ||
import { OPTIONS_TYPE } from './nest.module-definition'; | ||
|
||
/** | ||
* Workaround to dynamically set the path for the controller. | ||
* | ||
* A custom provider is necessary to ensure that the controller path is set during | ||
* application initialization, because NestJS does not support declaration of | ||
* paths after the application has been initialized. | ||
* | ||
* @see https://github.com/nestjs/nest/issues/1438#issuecomment-863446608 | ||
*/ | ||
export const registerApiPath: FactoryProvider = { | ||
provide: REGISTER_API_PATH, | ||
useFactory: (options: typeof OPTIONS_TYPE) => { | ||
if (!options.apiPath) { | ||
throw new Error('`apiPath` must be provided to set the controller path'); | ||
} | ||
|
||
Reflect.defineMetadata(PATH_METADATA, options.apiPath, NovuController); | ||
}, | ||
inject: [NOVU_OPTIONS], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export type SupportedFrameworkName = 'next' | 'express' | 'nuxt' | 'h3' | 'sveltekit' | 'remix' | 'lambda'; | ||
export type SupportedFrameworkName = 'next' | 'express' | 'nuxt' | 'h3' | 'sveltekit' | 'remix' | 'lambda' | 'nest'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ | |
"sourceMap": true, | ||
"rootDir": ".", | ||
"outDir": "./dist", | ||
"experimentalDecorators": true, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Necessary to use NestJS decorators. |
||
"strict": true | ||
}, | ||
"include": ["./src/**/*", "./package.json", "scripts/devtool.ts"], | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
NOVU_SECRET_KEY= | ||
NOVU_API_URL=https://api.novu.co |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
dist | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Novu NestJS Playground | ||
|
||
This project is a simple example of how to use Novu Framework with NestJS. | ||
|
||
## Quick start | ||
|
||
This quickstart assumes you are running this application from the Novu monorepo and have already installed the dependencies. | ||
|
||
Copy the `.env.example` file to `.env` and set the correct environment variables. | ||
|
||
```bash | ||
cp .env.example .env | ||
``` | ||
|
||
Then, run the application: | ||
|
||
```bash | ||
pnpm start | ||
``` | ||
|
||
Finally, start Novu Studio and follow the CLI instructions to start creating your NestJS notification workflows: | ||
|
||
```bash | ||
npx novu@latest dev | ||
``` | ||
|
||
## Testing | ||
|
||
```bash | ||
pnpm test | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
{ | ||
"$schema": "https://json.schemastore.org/nest-cli", | ||
"collection": "@nestjs/schematics", | ||
"sourceRoot": "src", | ||
"compilerOptions": { | ||
"typeCheck": true, | ||
"deleteOutDir": true, | ||
"builder": { | ||
"type": "swc", | ||
"options": { | ||
"stripLeadingPaths": true | ||
} | ||
}, | ||
"assets": [ | ||
{ | ||
"include": ".env", | ||
"outDir": "dist" | ||
}, | ||
{ | ||
"include": ".env.development", | ||
"outDir": "dist" | ||
}, | ||
{ | ||
"include": ".env.test", | ||
"outDir": "dist" | ||
}, | ||
{ | ||
"include": ".env.production", | ||
"outDir": "dist" | ||
} | ||
] | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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