NestboltNestbolt

@nestbolt/likeable

Events

Listen to like and unlike events via @nestjs/event-emitter.

@nestbolt/likeable emits events when entities are liked or unliked. Events are dispatched via @nestjs/event-emitter when it's installed and EventEmitterModule is registered.

Setup

Install and register the event emitter module:

npm install @nestjs/event-emitter
import { EventEmitterModule } from "@nestjs/event-emitter";
import { LikeableModule } from "@nestbolt/likeable";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    LikeableModule.forRoot(),
    // ...
  ],
})
export class AppModule {}

If @nestjs/event-emitter is not installed or EventEmitterModule is not registered, the package works normally but no events are emitted. The event emitter is resolved lazily and treated as fully optional.

Events

like.liked

Emitted after a successful like() call -- but only when an actual row was inserted. Duplicate likes (idempotent no-ops) do not emit.

import { OnEvent } from "@nestjs/event-emitter";
import { LIKEABLE_EVENTS, type LikedEvent } from "@nestbolt/likeable";

@Injectable()
export class LikeListener {
  @OnEvent(LIKEABLE_EVENTS.LIKED)
  handleLiked(event: LikedEvent) {
    console.log(`User ${event.userId} liked ${event.likeableType}:${event.likeableId}`);
  }
}

Payload: LikedEvent

FieldTypeDescription
likeableTypestringThe polymorphic type of the entity (from @Likeable({ type }) or class name)
likeableIdstringThe ID of the entity that was liked
userIdstringThe ID of the user who liked it

like.unliked

Emitted after a successful unlike() call -- but only when a row was actually deleted. Calling unlike() for a user who hadn't liked the entity is a no-op and does not emit.

import { OnEvent } from "@nestjs/event-emitter";
import { LIKEABLE_EVENTS, type UnlikedEvent } from "@nestbolt/likeable";

@Injectable()
export class LikeListener {
  @OnEvent(LIKEABLE_EVENTS.UNLIKED)
  handleUnliked(event: UnlikedEvent) {
    console.log(`User ${event.userId} unliked ${event.likeableType}:${event.likeableId}`);
  }
}

Payload: UnlikedEvent

FieldTypeDescription
likeableTypestringThe polymorphic type of the entity
likeableIdstringThe ID of the entity that was unliked
userIdstringThe ID of the user who removed their like

Event Constants

Use the LIKEABLE_EVENTS constant for type-safe event names:

import { LIKEABLE_EVENTS } from "@nestbolt/likeable";

LIKEABLE_EVENTS.LIKED;   // "like.liked"
LIKEABLE_EVENTS.UNLIKED; // "like.unliked"

Behavior with toggle()

toggle() is implemented in terms of like() and unlike(), so it emits exactly one of the two events per call -- whichever side of the toggle actually changed state:

await post.toggle(userId); // emits like.liked
await post.toggle(userId); // emits like.unliked

Practical Example: Notify the Author

A common use case is notifying the author of a post when someone likes it:

@Injectable()
export class LikeNotificationListener {
  constructor(
    @InjectRepository(Post) private readonly posts: Repository<Post>,
    private readonly notifications: NotificationsService,
  ) {}

  @OnEvent(LIKEABLE_EVENTS.LIKED)
  async handleLiked(event: LikedEvent) {
    if (event.likeableType !== "Post") return;

    const post = await this.posts.findOneBy({ id: event.likeableId });
    if (!post || post.authorId === event.userId) return;

    await this.notifications.send(post.authorId, {
      type: "post.liked",
      postId: post.id,
      likedBy: event.userId,
    });
  }
}

Filter on likeableType so a single listener can handle one entity type at a time, even though all likes share the like.liked event name.

Reliability

Events are emitted after the database write succeeds. If your handler throws, the like itself is unaffected -- the row is already committed. Events are best-effort signals, not transactional outbox messages.