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()exportclassPostextendsLikeableMixin(BaseEntity){
@PrimaryGeneratedColumn("uuid")id!: string;
@Column()title!: string;}// Like, unlike, toggleawaitlikeableService.like(Post,postId,userId);constcount=awaitlikeableService.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(),],})exportclassAppModule{}import{Likeable,LikeableMixin}from"@nestbolt/likeable";
@Entity("posts")
@Likeable()exportclassPostextendsLikeableMixin(BaseEntity){
@PrimaryGeneratedColumn("uuid")id!: string;
@Column()title!: string;}import{LikeableService}from"@nestbolt/likeable";
@Injectable()exportclassPostService{constructor(privatereadonlylikeableService: LikeableService){}asynclikePost(postId: string,userId: string){awaitthis.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)awaitlikeableService.like(Post,postId,userId);// Unlike an entityawaitlikeableService.unlike(Post,postId,userId);// Toggle — returns true if liked, false if unlikedconstisNowLiked=awaitlikeableService.toggle(Post,postId,userId);// Check if a user liked an entityconstliked=awaitlikeableService.isLikedBy(Post,postId,userId);// Get total likes count for an entityconstcount=awaitlikeableService.getLikesCount(Post,postId);// Get user IDs who liked an entityconstlikerIds=awaitlikeableService.getLikers(Post,postId);// Get entity IDs liked by a userconstlikedPostIds=awaitlikeableService.getUserLikes(Post,userId);// Get count of entities liked by a userconstuserLikesCount=awaitlikeableService.getUserLikesCount(Post,userId);The LikeableMixin adds convenience methods directly on your entity:
@Entity("posts")
@Likeable()exportclassPostextendsLikeableMixin(BaseEntity){// ...}// Usageconstpost=awaitpostRepo.findOneBy({ id });awaitpost.like(userId);awaitpost.unlike(userId);constisNowLiked=awaitpost.toggle(userId);constliked=awaitpost.isLikedBy(userId);constcount=awaitpost.getLikesCount();constlikers=awaitpost.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()exportclassPostService{constructor(privatereadonlylikeableService: LikeableService){}asyncgetPostWithLikeStatus(postId: string,userId: string){constpost=awaitthis.postRepo.findOneBy({id: postId});constisLiked=awaitthis.likeableService.isLikedBy(Post,postId,userId);constlikesCount=awaitthis.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.