https://github.com/iris-events/nest-iris
https://github.com/iris-events/nest-iris
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/iris-events/nest-iris
- Owner: iris-events
- License: apache-2.0
- Created: 2024-03-07T15:15:07.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2024-05-07T05:33:17.000Z (about 1 year ago)
- Last Synced: 2024-05-07T18:06:51.184Z (about 1 year ago)
- Language: TypeScript
- Size: 141 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 5
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# IRIS NestJS module
NestJS package for IRIS framework.
Install: `npm i @iris-events/nestjs-iris`
## NestJS module setup
```ts
import { IrisModule } from '@iris-events/nestjs-iris'@Module({
imports: [
IrisModule.forConfig({
urlOrOpts: 'amqp://guest:guest@localhost:5672'
...
}),
...
],
...
})
class AppModule {}
```Options interface
```ts
// configuration interface for IrisModule:
interface IrisOptionsI {
// default is `npm_package_name`
serviceName?: string// AMQP Connection options
urlOrOpts: string | {
protocol?: string
hostname?: string
port?: number | string
username?: string
password?: string
locale?: string
frameMax?: number
heartbeat?: number
vhost?: string
}/**
* Passed to amqplib, see amqplib doc for more info
*/
socketOptions?: any/**
* How many times should iris try to reconnect when connection drops
* setting to 0 or less means do not try to reconnect
*/
reconnectTries: number/**
* Reconnect interval
*/reconnectInterval: number
/**
* Multiply factor for reconnectInterval for each next time if reconnecting fails
*/
reconnectFactor: number/**
* When error occures during event processing, event is re-enqueued.
* This setting specifies how many times should a single event be re-enqueued
* before marked as failed.
*/
maxMessageRetryCount: number// Global setting for message validation. Can be set per message via @Message decorator
defaultValidationOptions?: {
// Objects (messages) are transformed to corresponding classes before publishing
// and when received using class transformer module.
// See https://github.com/typestack/class-transformer for more info
classTransform?: classTransformer.ClassTransformOptions// If false, object will not be validated against the decorated @Message class
// structure, neither when publishing nor when received
validate?: boolean | classValidator.ValidatorOptions
}
}```
## Decorators
### @Message(config: MessageI, validationOptions?: ValidationOptions)
Message config interface
```ts
interface MessageI {
/**
* Name of exchange
*/
name: string
/**
* Defaults to `fanout`
*/
exchangeType?: ExchangeType
routingKey?: string
scope?: Scope
ttl?: number
deadLetter?: string
/**
* Max times this message should be sent to retry queue
* before considered unhandled. Overrides default from
* connection configuration.
*/
maxRetry?: number
}```
Class decorator, representing a specific message.
[Class transformer](https://github.com/typestack/class-transformer) and
[class validator](https://github.com/typestack/class-validator) are used to
transform objects to decorated classes.Validation of messages and transformer options can be set globally via config or
per message as a second argument with `@Message` decorator.```ts
@Message({ name: 'foo' })
class Foo {
@IsString() foo!: string
}
```
### @MessageHandler(config?: MessageHandlerI, returnMessageType?: @Message())
Handler config interface
```ts
interface MessageHandlerI {
bindingKeys?: string[] | string
/**
* If true, the queue will survive broker restarts (defaults to true)
*/
durable?: boolean
/**
* If true, the queue will be deleted when the number of consumers drops to zero
* (defaults to false)
*/
autoDelete?: boolean
/**
* Amount of messages that can be received on queue at same time.
* Set it to some low number (like 1) for events causing a long/resource heavy
* tasks.
*/
prefetch?: number
messageDeliveryMode?: MessageDeliveryMode
}
```Method decorator handling specific `@Message` events. *Method must accept exactly one argument of class decorated with `@Message`.*
Special argument classes:
- any class decorated with @Message() can be used within the handler without any extra argument decorators.
- a `AmqpMessage` class can be used to retrieve original Amqp message within the Handler
- a `MDC` class can be used to retrieve an object with common "mdc" properties (usefull for logging compatible with Java IRIS counterparts)If handler returns a result, it is treated as a produced message and is published if `returnMessageType` is specified for handler.
- Minimal handler requirement
```tsclass Handler {
@MessageHandler()
handleFoo(foo: Foo): void {}}
```- Special arguments and custom decorators
```ts// custom param decorator
const MsgHeaderExtractorParam = createParamDecorator<
string,
ExecutionContext,
string | undefined
>((header, ctx): string => {
const msg = ctx.switchToRpc().getData()return msg.properties.headers![header]
})class Handler {
@MessageHandler()
handleFoo(
// original Amqp message
// holds
// {
// content: Buffer
// fields: amqplib.ConsumeMessageFields
// properties: amqplib.MessageProperties
// }
amqpMessage: AmqpMessage,
// holds
// {
// sessionId?: string
// userId?: string
// clientTraceId?: string
// correlationId?: string
// eventType?: string
// clientVersion?: string
// }
mdc: MDC,
// event we're handling
foo: Foo,// custom arguments
@MsgHeaderExtractorParam('x-y') xyHeader: string,
): void {}}
```- Handler produces another message
```ts
@Message({ name: 'bar' })
class Bar {
@IsString() bar!: string
}class Handler {
// Type of returned message needs to be specified as a second argument
// of decorator
@MessageHandler({}, Bar)
handleFoo(foo: Foo): Bar {
return { bar: 'bar bar' }
}}
```
### @SnapshotMessageHandler(config?: SnapshotMessageHandlerI)
Snapshot handler config interface
```ts
interface SnapshotMessageHandlerI {
resourceType: string
prefetch?: number
}
```Method decorator handling `SnapshotRequested` messages. Each resourceType should
have its own handler.```ts
class Handler {
@SnapshotMessageHandler({ resourceType: 'foo' }, ResourceMessage)
handleFooSnapshotRequest(evt: SnapshotRequested): ResourceMessage {
return {
...evt,
payload: { foo model for evt.resourceId }
}
}@SnapshotMessageHandler({ resourceType: 'bar' }, ResourceMessage)
handleFooSnapshotRequest(evt: SnapshotRequested): ResourceMessage {
return {
...evt,
payload: { bar model for evt.resourceId }
}
}
}
```## Health Indicator
IRIS module provides `IrisHealthModule` and `IrisHealthIndicator` which can be used with `@nestjs/terminus` module.
## Testing
`IrisTestModule` can be used to register IRIS module for testing. This will replace the default [IrisServer](src/iris.server.ts) with [IrisTesstServer](src/testing/iris.test-server.ts), which provides testing utilities:
- clearQueues()
- getSpyForHandlder(, )
- getIrisHandlers()
- getIrisHandlerMetas()`testing` submodule also exports whole testing submodule from `@iris-events/iris/testing` submodule as `irisTesting`.
```ts
import { IrisTestModule, irisTesting } from '@iris-events/nestjs-iris/testing'// configure within NestJS test module
IrisTestModule.forConfig(
{
urlOrOpts: 'amqp://guest:guest@localhost:5672'
...
},
// mock function
vi.fn() // or jest.fn etc
)// get IrisTestServer instance
const testServer = app.get(IrisTestServer).getSpyForMessage(MyMessage)const spy = testServer.getSpyForHandlder(MyHandler, 'handleFoo')
// access @iris-events/testing utilities
irisTesting.utilities.deleteExchange(...)
irisTesting.utilities.requestSnapshot(...)
irisTesting.utilities.subscribe(...)
irisTesting.utilities.publishToFrontend(...)```
## Examples
See [examples](examples) and [tests](test/e2e) folders for additional info. Example application can be run using `npm run example`Example
```ts
import { Controller, Logger, Module } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { IsNumber, IsString } from 'class-validator'
import {
IrisModule,
IrisServer,
Message,
MessageHandler,
publish,
} from '@iris-events/nestjs-iris'@Message({ name: 'ping' })
class Ping {
@IsString() ping!: string
@IsNumber() count!: number
}@Message({ name: 'pong' })
class Pong {
@IsString() pong!: string
@IsNumber() count!: number
}@Controller()
class PingController {
private readonly logger = new Logger('PingController')@MessageHandler()
async handlePing(msg: Ping) {
this.logger.log('Received ping', msg.ping, msg.count)
await this.sleep()
publish.getPublisher(Pong)({ pong: 'pong', count: msg.count + 1 })
}@MessageHandler(undefined, Ping)
async handlePong(msg: Pong): Promise {
this.logger.log('Received pong', msg.pong, msg.count)
await this.sleep()
return { ping: 'ping', count: msg.count + 1 }
}async sleep() {
await new Promise((resolve) => setTimeout(resolve, 1500))
}
}@Module({
imports: [
IrisModule.forRoot({
urlOrOpts: 'amqp://localhost',
}),
],
controllers: [PingController],
})
class App {}```