Skip to content

nestbolt/likeable

Repository files navigation

@nestbolt/likeable

Polymorphic like/favorite/bookmark system for NestJS with TypeORM — like any entity.

npm version npm downloads tests license


This package provides a polymorphic like/favorite system for NestJS that lets users like, favorite, or bookmark any entity with user-scoped uniqueness and like counts.

Once installed, using it is as simple as:

@Entity("posts")
@Likeable()
export class Post extends LikeableMixin(BaseEntity) {
  @PrimaryGeneratedColumn("uuid") id!: string;
  @Column() title!: string;
}

// Like, unlike, toggle
await likeableService.like(Post, postId, userId);
const count = await likeableService.getLikesCount(Post, postId);

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/likeable

Or via yarn:

yarn add @nestbolt/likeable

Or via pnpm:

pnpm add @nestbolt/likeable

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

npm install @nestjs/event-emitter   # For like.liked, like.unliked events

Quick Start

1. Register the module in your AppModule

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

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

2. Mark entities as likeable

import { Likeable, LikeableMixin } from "@nestbolt/likeable";

@Entity("posts")
@Likeable()
export class Post extends LikeableMixin(BaseEntity) {
  @PrimaryGeneratedColumn("uuid") id!: string;
  @Column() title!: string;
}

3. Use the service to like entities

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

@Injectable()
export class PostService {
  constructor(private readonly likeableService: LikeableService) {}

  async likePost(postId: string, userId: string) {
    await this.likeableService.like(Post, postId, userId);
  }
}

Module Configuration

The module is registered globally — you only need to import it once.

Static Configuration (forRoot)

LikeableModule.forRoot();

Async Configuration (forRootAsync)

LikeableModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({}),
});

Using the @Likeable() Decorator

The @Likeable() class decorator marks an entity for polymorphic liking:

@Likeable()                       // type defaults to class name
@Likeable({ type: "BlogPost" })   // custom type override
Option Type Default Description
type string Class name Override the entity type name in likes table

Like Operations

// Like an entity (idempotent — won't duplicate)
await likeableService.like(Post, postId, userId);

// Unlike an entity
await likeableService.unlike(Post, postId, userId);

// Toggle — returns true if liked, false if unliked
const isNowLiked = await likeableService.toggle(Post, postId, userId);

Query Methods

// Check if a user liked an entity
const liked = await likeableService.isLikedBy(Post, postId, userId);

// Get total likes count for an entity
const count = await likeableService.getLikesCount(Post, postId);

// Get user IDs who liked an entity
const likerIds = await likeableService.getLikers(Post, postId);

// Get entity IDs liked by a user
const likedPostIds = await likeableService.getUserLikes(Post, userId);

// Get count of entities liked by a user
const userLikesCount = await likeableService.getUserLikesCount(Post, userId);

Entity Mixin

The LikeableMixin adds convenience methods directly on your entity:

@Entity("posts")
@Likeable()
export class Post extends LikeableMixin(BaseEntity) {
  // ...
}

// Usage
const post = await postRepo.findOneBy({ id });
await post.like(userId);
await post.unlike(userId);
const isNowLiked = await post.toggle(userId);
const liked = await post.isLikedBy(userId);
const count = await post.getLikesCount();
const likers = await post.getLikers();
Method Returns Description
like(userId) Promise<void> Like this entity
unlike(userId) Promise<void> Unlike this entity
toggle(userId) Promise<boolean> Toggle like status
isLikedBy(userId) Promise<boolean> Check if user liked entity
getLikesCount() Promise<number> Get total likes count
getLikers() Promise<string[]> Get user IDs of likers

Events

When @nestjs/event-emitter is installed, the package emits:

Event Payload When
like.liked { likeableType, likeableId, userId } After an entity is liked
like.unliked { likeableType, likeableId, userId } After an entity is unliked
import { LIKEABLE_EVENTS, LikedEvent } from "@nestbolt/likeable";
import { OnEvent } from "@nestjs/event-emitter";

@OnEvent(LIKEABLE_EVENTS.LIKED)
handleLiked(event: LikedEvent) {
  console.log(`${event.likeableType}#${event.likeableId} liked by ${event.userId}`);
}

Using the Service Directly

Inject LikeableService for like management and querying:

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

@Injectable()
export class PostService {
  constructor(private readonly likeableService: LikeableService) {}

  async getPostWithLikeStatus(postId: string, userId: string) {
    const post = await this.postRepo.findOneBy({ id: postId });
    const isLiked = await this.likeableService.isLikedBy(Post, postId, userId);
    const likesCount = await this.likeableService.getLikesCount(Post, postId);
    return { ...post, isLiked, likesCount };
  }
}
Method Returns Description
like(Entity, entityId, userId) Promise<void> Like an entity
unlike(Entity, entityId, userId) Promise<void> Unlike an entity
toggle(Entity, entityId, userId) Promise<boolean> Toggle like status
isLikedBy(Entity, entityId, userId) Promise<boolean> Check if user liked entity
getLikesCount(Entity, entityId) Promise<number> Get total likes count
getLikers(Entity, entityId) Promise<string[]> Get user IDs of likers
getUserLikes(Entity, userId) Promise<string[]> Get entity IDs liked by user
getUserLikesCount(Entity, userId) Promise<number> Count entities liked by user
isLikeable(Entity) boolean Check for @Likeable metadata

Like Entity

The likes table stores:

Column Type Description
id UUID Primary key
likeable_type varchar(255) Entity type name
likeable_id varchar(36) Entity ID
user_id varchar(36) User who liked
created_at timestamp When the like was created

A unique index on (user_id, likeable_type, likeable_id) ensures one like per user per entity.

Testing

npm test

Run tests in watch mode:

npm run test:watch

Generate coverage report:

npm run test:cov

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.

About

Polymorphic like/favorite/bookmark system for NestJS with TypeORM — like any entity.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors