NestboltNestbolt

@nestbolt/notifications

Notifiable Entities

Turn any TypeORM entity into a notification recipient using the @Notifiable() decorator and NotifiableMixin, with methods for sending and querying notifications.

A "notifiable entity" is any TypeORM entity that can receive notifications. The package provides a @Notifiable() decorator and a NotifiableMixin function that together add notification capabilities directly to your entities.

Setting Up a Notifiable Entity

To make an entity notifiable, apply two things:

  1. The @Notifiable() decorator -- registers metadata about the entity type.
  2. NotifiableMixin(BaseEntity) -- adds notification methods to the entity class.
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()
  name!: string;

  @Column()
  email!: string;
}

The @Notifiable() Decorator

The @Notifiable() decorator stores metadata on the entity class that identifies its "notifiable type" -- the string stored in the notifiable_type column of the notifications table.

Default Behavior

By default, the notifiable type is the class name:

@Entity("users")
@Notifiable()
export class User extends NotifiableMixin(BaseEntity) {
  // getNotifiableType() returns "User"
}

Custom Notifiable Type

You can override the type by passing notifiableType in the options:

@Entity("users")
@Notifiable({ notifiableType: "AppUser" })
export class User extends NotifiableMixin(BaseEntity) {
  // getNotifiableType() returns "AppUser"
}

This is useful when:

  • Your class name might change during refactoring, but you want the stored type to remain stable.
  • You have multiple entity classes that should share a notifiable type.
  • You want a more readable or namespace-aware identifier.

NotifiableOptions

The decorator accepts an optional NotifiableOptions object:

interface NotifiableOptions {
  notifiableType?: string;
}
PropertyTypeDefaultDescription
notifiableTypestringThe class nameThe type string stored in the database.

The NotifiableMixin

The NotifiableMixin function is a TypeScript mixin that adds notification methods to any class. It is typically used with TypeORM's BaseEntity:

export class User extends NotifiableMixin(BaseEntity) {
  // ...
}

The mixin can also wrap other base classes if you have a custom entity base:

class SoftDeletableEntity extends BaseEntity {
  @Column({ nullable: true })
  deletedAt?: Date;
}

@Entity("users")
@Notifiable()
export class User extends NotifiableMixin(SoftDeletableEntity) {
  // Inherits both SoftDeletableEntity and NotifiableMixin methods
}

Mixin Methods

The mixin adds the following methods to your entity:

getNotifiableType()

Returns the notifiable type string. Uses the @Notifiable() decorator metadata if available, otherwise falls back to the class name.

const type = user.getNotifiableType();
// "User" (or "AppUser" if @Notifiable({ notifiableType: "AppUser" }) is used)

getNotifiableId()

Returns the entity's ID as a string. The mixin reads the id property and converts it with String():

const id = user.getNotifiableId();
// "550e8400-e29b-41d4-a716-446655440000"

This works with UUIDs, numeric IDs, and any other ID type.

notify(notification)

Sends a notification to this entity. This is a convenience method that calls NotificationService.send() internally:

await user.notify(new WelcomeNotification(user));

This is equivalent to:

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

The NotificationService instance is resolved via a static singleton pattern -- the service registers itself on module initialization and is accessed by the mixin at runtime. If NotificationModule.forRoot() has not been imported, calling notify() will throw:

NotificationModule has not been initialized. Make sure NotificationModule.forRoot() is imported.

getNotifications()

Returns all notifications for this entity, ordered by creation date (newest first):

const notifications = await user.getNotifications();

This calls NotificationService.getNotifications() with getNotifiableType() and getNotifiableId().

unreadNotifications()

Returns only unread notifications for this entity (where read_at is null):

const unread = await user.unreadNotifications();

getUnreadNotificationCount()

Returns the count of unread notifications:

const count = await user.getUnreadNotificationCount();
// 12

markNotificationAsRead(notificationId)

Marks a specific notification as read:

await user.markNotificationAsRead("notification-uuid");

markNotificationAsUnread(notificationId)

Marks a specific notification as unread:

await user.markNotificationAsUnread("notification-uuid");

markAllNotificationsAsRead()

Marks all of this entity's unread notifications as read in a single database update:

await user.markAllNotificationsAsRead();

The NotifiableMixinEntity Interface

The mixin implements the NotifiableMixinEntity interface, which defines all the methods added by the mixin:

interface NotifiableMixinEntity {
  getNotifiableType(): string;
  getNotifiableId(): string;
  notify(notification: Notification): Promise<void>;
  getNotifications(): Promise<NotificationEntity[]>;
  unreadNotifications(): Promise<NotificationEntity[]>;
  markNotificationAsRead(notificationId: string): Promise<void>;
  markNotificationAsUnread(notificationId: string): Promise<void>;
  markAllNotificationsAsRead(): Promise<void>;
  getUnreadNotificationCount(): Promise<number>;
}

The NotifiableEntity Interface

The NotifiableEntity interface defines the contract that the NotificationService and channels expect from a notifiable entity. The mixin satisfies this interface automatically:

interface NotifiableEntity {
  id: string;
  email?: string;

  getNotifiableType(): string;
  getNotifiableId(): string;

  routeNotificationFor?(channel: string): string | undefined;

  notify?(notification: Notification): Promise<void>;
  getNotifications?(): Promise<NotificationEntity[]>;
  unreadNotifications?(): Promise<NotificationEntity[]>;
  markNotificationAsRead?(notificationId: string): Promise<void>;
  markNotificationAsUnread?(notificationId: string): Promise<void>;
  markAllNotificationsAsRead?(): Promise<void>;
  getUnreadNotificationCount?(): Promise<number>;
}

The required properties are id, getNotifiableType(), and getNotifiableId(). All other properties and methods are optional.

routeNotificationFor()

The routeNotificationFor() method tells channels how to reach the entity. Each channel can define its own routing key:

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

  @Column()
  email!: string;

  @Column({ nullable: true })
  phone?: string;

  @Column({ nullable: true })
  slackUserId?: string;

  @Column({ nullable: true })
  fcmToken?: string;

  routeNotificationFor(channel: string): string | undefined {
    switch (channel) {
      case "mail":
        return this.email;
      case "sms":
        return this.phone;
      case "slack":
        return this.slackUserId;
      case "push":
        return this.fcmToken;
      default:
        return undefined;
    }
  }
}

The mail channel specifically calls routeNotificationFor("mail") and falls back to the email property if the method is not defined or returns undefined.

Multiple Notifiable Entity Types

You can have multiple notifiable entities in your application. Each one tracks notifications independently:

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

  @Column()
  email!: string;
}

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

  @Column()
  adminEmail!: string;

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

Notifications are scoped by notifiable_type and notifiable_id, so User and Organization notifications are completely separate:

// Send to a user
await notificationService.send(user, new WelcomeNotification(user));

// Send to an organization
await notificationService.send(org, new NewMemberNotification(member));

// Query user notifications
const userNotifications = await notificationService.getNotifications("User", userId);

// Query organization notifications
const orgNotifications = await notificationService.getNotifications("Organization", orgId);

Complete Example

// entities/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Notifiable, NotifiableMixin } from "@nestbolt/notifications";

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

  @Column()
  name!: string;

  @Column({ unique: true })
  email!: string;

  @Column({ nullable: true })
  phone?: string;

  routeNotificationFor(channel: string): string | undefined {
    if (channel === "mail") return this.email;
    if (channel === "sms") return this.phone;
    return undefined;
  }
}
// users/user.service.ts
import { Injectable } from "@nestjs/common";
import { User } from "./user.entity";
import { WelcomeNotification } from "../notifications/welcome.notification";

@Injectable()
export class UserService {
  async register(name: string, email: string): Promise<User> {
    const user = new User();
    user.name = name;
    user.email = email;
    await user.save();

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

    return user;
  }

  async getNotificationSummary(user: User) {
    const total = (await user.getNotifications()).length;
    const unread = await user.getUnreadNotificationCount();

    return { total, unread };
  }

  async markAllAsRead(user: User): Promise<void> {
    await user.markAllNotificationsAsRead();
  }
}