npm package: @nestbolt/notifications
Description: Multi-channel notification system for NestJS — database, email, and custom channels with a fluent API.
Latest version: 0.1.0

<p align="center">
    <h1 align="center">@nestbolt/notifications</h1>
    <p align="center">Multi-channel notification system for NestJS — send notifications via database, email, and custom channels with a fluent API.</p>
</p>

<p align="center">
    <a href="https://www.npmjs.com/package/@nestbolt/notifications"><img src="https://img.shields.io/npm/v/@nestbolt/notifications.svg?style=flat-square" alt="npm version"></a>
    <a href="https://www.npmjs.com/package/@nestbolt/notifications"><img src="https://img.shields.io/npm/dt/@nestbolt/notifications.svg?style=flat-square" alt="npm downloads"></a>
    <a href="https://github.com/nestbolt/notifications/actions"><img src="https://img.shields.io/github/actions/workflow/status/nestbolt/notifications/tests.yml?branch=main&style=flat-square&label=tests" alt="tests"></a>
    <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square" alt="license"></a>
</p>

<hr>

This package provides a **notification system** for [NestJS](https://nestjs.com) that sends notifications through multiple channels (database, email, custom) using a clean class-based API with TypeORM integration.

Once installed, using it is as simple as:

```typescript
await notificationService.send(user, new WelcomeNotification(user));

// Or via the entity mixin
await user.notify(new WelcomeNotification(user));

// Query notifications
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();
```

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Module Configuration](#module-configuration)
  - [Static Configuration (forRoot)](#static-configuration-forroot)
  - [Async Configuration (forRootAsync)](#async-configuration-forrootasync)
- [Creating Notifications](#creating-notifications)
- [Sending Notifications](#sending-notifications)
- [Database Channel](#database-channel)
- [Mail Channel](#mail-channel)
  - [MailMessage Builder](#mailmessage-builder)
- [Custom Channels](#custom-channels)
- [Entity Decorator & Mixin](#entity-decorator--mixin)
- [Querying Notifications](#querying-notifications)
- [Read/Unread Tracking](#readunread-tracking)
- [Events](#events)
- [Configuration Options](#configuration-options)
- [Testing](#testing)
- [Changelog](#changelog)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)

## Installation

Install the package via npm:

```bash
npm install @nestbolt/notifications
```

Or via yarn:

```bash
yarn add @nestbolt/notifications
```

Or via pnpm:

```bash
pnpm add @nestbolt/notifications
```

### Peer Dependencies

This package requires the following peer dependencies, which you likely already have in a NestJS project:

```
@nestjs/common      ^10.0.0 || ^11.0.0
@nestjs/core        ^10.0.0 || ^11.0.0
@nestjs/typeorm     ^10.0.0 || ^11.0.0
typeorm             ^0.3.0
reflect-metadata    ^0.1.13 || ^0.2.0
```

Optional peer dependencies:

```
nodemailer               ^6.0.0              (for the mail channel)
@nestjs/event-emitter    ^2.0.0 || ^3.0.0   (for lifecycle events)
```

## Quick Start

### 1. Register the module in your `AppModule`

```typescript
import { NotificationModule } from "@nestbolt/notifications";

@Module({
  imports: [
    NotificationModule.forRoot({
      channels: {
        database: true,
      },
    }),
  ],
})
export class AppModule {}
```

### 2. Create a notification class

```typescript
import { Notification, MailMessage } from "@nestbolt/notifications";

class WelcomeNotification extends Notification {
  constructor(private user: { name: string }) {
    super();
  }

  via(): string[] {
    return ["database"];
  }

  toDatabase() {
    return { message: `Welcome ${this.user.name}!`, type: "welcome" };
  }
}
```

### 3. Send a notification

```typescript
import { NotificationService } from "@nestbolt/notifications";

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

  async onboardUser(user: NotifiableEntity): Promise<void> {
    await this.notifications.send(user, new WelcomeNotification(user));
  }
}
```

## Module Configuration

### Static Configuration (forRoot)

```typescript
NotificationModule.forRoot({
  channels: {
    database: true,
    mail: {
      transport: {
        host: "smtp.example.com",
        port: 587,
        auth: { user: "user", pass: "pass" },
      },
      defaults: { from: "noreply@example.com" },
    },
  },
});
```

### Async Configuration (forRootAsync)

```typescript
NotificationModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    channels: {
      database: true,
      mail: {
        transport: {
          host: config.get("SMTP_HOST"),
          port: config.get("SMTP_PORT"),
          auth: {
            user: config.get("SMTP_USER"),
            pass: config.get("SMTP_PASS"),
          },
        },
        defaults: { from: config.get("MAIL_FROM") },
      },
    },
  }),
});
```

The module is registered as **global** — you don't need to import it in every module.

## Creating Notifications

Each notification is a class extending `Notification`:

```typescript
import { Notification, MailMessage } from "@nestbolt/notifications";

class OrderShippedNotification extends Notification {
  constructor(
    private order: { id: string; trackingUrl: string },
  ) {
    super();
  }

  via(): string[] {
    return ["database", "mail"];
  }

  toDatabase() {
    return {
      message: `Your order #${this.order.id} has shipped!`,
      orderId: this.order.id,
    };
  }

  toMail() {
    return new MailMessage()
      .subject("Your order has shipped!")
      .greeting("Hello")
      .line(`Your order #${this.order.id} is on its way.`)
      .action("Track Order", this.order.trackingUrl)
      .salutation("Thank you for your purchase");
  }
}
```

## Sending Notifications

### Send to a single recipient

```typescript
await notificationService.send(user, new OrderShippedNotification(order));
```

### Send to multiple recipients

```typescript
await notificationService.sendToMany(users, new OrderShippedNotification(order));
```

## Database Channel

The database channel stores notifications in a `notifications` table. It's enabled by default.

The `NotificationEntity` has the following columns:

| Column            | Type          | Description                    |
| ----------------- | ------------- | ------------------------------ |
| `id`              | `uuid`        | Primary key                    |
| `type`            | `varchar`     | Notification class name        |
| `notifiable_type` | `varchar`     | Entity type (e.g., "User")     |
| `notifiable_id`   | `varchar`     | Entity ID                      |
| `channel`         | `varchar`     | Channel name                   |
| `data`            | `simple-json` | Notification payload           |
| `read_at`         | `datetime`    | When marked as read (nullable) |
| `created_at`      | `datetime`    | Created timestamp              |
| `updated_at`      | `datetime`    | Updated timestamp              |

## Mail Channel

The mail channel sends emails via [nodemailer](https://nodemailer.com/). Install it as a peer dependency:

```bash
pnpm add nodemailer
```

The recipient email is resolved in order:
1. `notifiable.routeNotificationFor("mail")` — if the method exists and returns a value
2. `notifiable.email` — fallback

### MailMessage Builder

Build email content with the fluent `MailMessage` API:

```typescript
new MailMessage()
  .subject("Welcome!")
  .from("custom@example.com")
  .replyTo("support@example.com")
  .cc("manager@example.com")
  .cc(["team1@example.com", "team2@example.com"])
  .bcc("archive@example.com")
  .greeting("Hello John")
  .line("Thanks for joining our platform.")
  .line("Here are some next steps.")
  .action("Get Started", "https://example.com/dashboard")
  .salutation("Best regards")
  .attach({ filename: "guide.pdf", path: "/path/to/guide.pdf" });
```

The builder generates both HTML and plain text versions automatically.

## Custom Channels

Register custom channels to send notifications via any transport:

```typescript
import { Injectable } from "@nestjs/common";
import type { NotificationChannel, NotifiableEntity, Notification } from "@nestbolt/notifications";

@Injectable()
class SlackChannel implements NotificationChannel {
  async send(notifiable: NotifiableEntity, notification: Notification): Promise<void> {
    const data = (notification as any).toSlack();
    // Send to Slack...
  }
}

// Register in module (static)
NotificationModule.forRoot({
  channels: {
    database: true,
    custom: { slack: SlackChannel },
  },
});

// Or with async configuration
NotificationModule.forRootAsync({
  useFactory: () => ({
    channels: { database: true },
  }),
  customChannels: { slack: SlackChannel },
});
```

Then use it in your notification:

```typescript
class AlertNotification extends Notification {
  via(): string[] {
    return ["database", "slack"];
  }

  toSlack() {
    return { text: "Alert!", channel: "#alerts" };
  }
}
```

## Entity Decorator & Mixin

Add notification methods directly to your entities:

```typescript
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Notifiable, NotifiableMixin } from "@nestbolt/notifications";

@Entity("users")
@Notifiable()
export class User extends NotifiableMixin(BaseEntity) {
  @PrimaryGeneratedColumn("uuid")
  id!: string;

  @Column()
  email!: string;

  routeNotificationFor(channel: string): string | undefined {
    if (channel === "mail") return this.email;
    return undefined;
  }
}
```

Then use directly on entity instances:

```typescript
// Send a notification
await user.notify(new WelcomeNotification(user));

// Query notifications
const all = await user.getNotifications();
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();

// Mark as read
await user.markNotificationAsRead(notification.id);
await user.markAllNotificationsAsRead();
```

The `@Notifiable()` decorator accepts an optional `notifiableType` override:

```typescript
@Notifiable({ notifiableType: "AppUser" })
```

## Querying Notifications

### Via the service

```typescript
// All notifications for an entity
const all = await notificationService.getNotifications("User", userId);

// Unread only
const unread = await notificationService.getUnreadNotifications("User", userId);

// Unread count
const count = await notificationService.getUnreadCount("User", userId);
```

### Via the mixin

```typescript
const all = await user.getNotifications();
const unread = await user.unreadNotifications();
const count = await user.getUnreadNotificationCount();
```

## Read/Unread Tracking

```typescript
// Mark a single notification as read
await notificationService.markAsRead(notificationId);

// Mark a notification as unread
await notificationService.markAsUnread(notificationId);

// Mark all notifications as read for an entity
await notificationService.markAllAsRead("User", userId);

// Delete a notification
await notificationService.deleteNotification(notificationId);

// Via the mixin
await user.markNotificationAsRead(notificationId);
await user.markNotificationAsUnread(notificationId);
await user.markAllNotificationsAsRead();
```

## Events

When `@nestjs/event-emitter` is installed, the following events are emitted:

| Event                    | Payload                                           |
| ------------------------ | ------------------------------------------------- |
| `notification.sending`   | `{ notifiable, notification, channel }`            |
| `notification.sent`      | `{ notifiable, notification, channel }`            |
| `notification.failed`    | `{ notifiable, notification, channel, error }`     |
| `notification.read`      | `{ notification }`                                 |
| `notification.all-read`  | `{ notifiableType, notifiableId }`                 |

```typescript
import { OnEvent } from "@nestjs/event-emitter";
import { NOTIFICATION_EVENTS, NotificationSentEvent } from "@nestbolt/notifications";

@Injectable()
export class NotificationListener {
  @OnEvent(NOTIFICATION_EVENTS.SENT)
  handleNotificationSent(event: NotificationSentEvent) {
    console.log(`Notification sent to ${event.notifiable.getNotifiableId()} via ${event.channel}`);
  }
}
```

## Configuration Options

| Option                  | Type                                    | Default | Description                 |
| ----------------------- | --------------------------------------- | ------- | --------------------------- |
| `channels.database`     | `boolean`                               | `true`  | Enable database channel     |
| `channels.mail`         | `MailChannelOptions`                    | —       | Mail channel configuration  |
| `channels.custom`       | `Record<string, Type<NotificationChannel>>` | —   | Custom channel classes      |

### MailChannelOptions

| Option             | Type     | Description             |
| ------------------ | -------- | ----------------------- |
| `transport.host`   | `string` | SMTP host               |
| `transport.port`   | `number` | SMTP port               |
| `transport.secure` | `boolean`| Use TLS                 |
| `transport.auth`   | `object` | `{ user, pass }`        |
| `defaults.from`    | `string` | Default sender address  |

## Service API

| Method                                    | Returns                           | Description                   |
| ----------------------------------------- | --------------------------------- | ----------------------------- |
| `send(notifiable, notification)`          | `Promise<void>`                   | Send to one recipient         |
| `sendToMany(notifiables, notification)`   | `Promise<void>`                   | Send to multiple recipients   |
| `getNotifications(type, id)`              | `Promise<NotificationEntity[]>`   | Get all notifications         |
| `getUnreadNotifications(type, id)`        | `Promise<NotificationEntity[]>`   | Get unread notifications      |
| `getUnreadCount(type, id)`               | `Promise<number>`                 | Get unread count              |
| `markAsRead(id)`                         | `Promise<void>`                   | Mark as read                  |
| `markAsUnread(id)`                       | `Promise<void>`                   | Mark as unread                |
| `markAllAsRead(type, id)`               | `Promise<void>`                   | Mark all as read              |
| `deleteNotification(id)`                | `Promise<void>`                   | Delete a notification         |

## Testing

```bash
pnpm test
```

Run tests in watch mode:

```bash
pnpm test:watch
```

Generate coverage report:

```bash
pnpm test:cov
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

## Security

If you discover any security-related issues, please report them via [GitHub Issues](https://github.com/nestbolt/notifications/issues) with the **security** label instead of using the public issue tracker.

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.