Polymorphic like/favorite/bookmark system for NestJS with TypeORM — like any entity.
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);- Installation
- Quick Start
- Module Configuration
- Using the @Likeable() Decorator
- Like Operations
- Query Methods
- Entity Mixin
- Events
- Using the Service Directly
- Like Entity
- Testing
- Changelog
- Contributing
- Security
- Credits
- License
Install the package via npm:
npm install @nestbolt/likeableOr via yarn:
yarn add @nestbolt/likeableOr via pnpm:
pnpm add @nestbolt/likeableThis 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
npm install @nestjs/event-emitter # For like.liked, like.unliked eventsimport { LikeableModule } from "@nestbolt/likeable";
@Module({
imports: [
TypeOrmModule.forRoot({ /* ... */ }),
LikeableModule.forRoot(),
],
})
export class AppModule {}import { Likeable, LikeableMixin } from "@nestbolt/likeable";
@Entity("posts")
@Likeable()
export class Post extends LikeableMixin(BaseEntity) {
@PrimaryGeneratedColumn("uuid") id!: string;
@Column() title!: string;
}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);
}
}The module is registered globally — you only need to import it once.
LikeableModule.forRoot();LikeableModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({}),
});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 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);// 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);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 |
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}`);
}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 |
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.
npm testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
If you discover any security-related issues, please report them via GitHub Issues with the security label instead of using the public issue tracker.
- Inspired by Laravel's likeable packages and overtrue/laravel-like
The MIT License (MIT). Please see License File for more information.