@nestbolt/likeable
v0.1.0
Published
Polymorphic like/favorite/bookmark system for NestJS with TypeORM — like any entity.
Maintainers
Readme
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
- 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
Installation
Install the package via npm:
npm install @nestbolt/likeableOr via yarn:
yarn add @nestbolt/likeableOr via pnpm:
pnpm add @nestbolt/likeablePeer 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.0Optional
npm install @nestjs/event-emitter # For like.liked, like.unliked eventsQuick 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 testRun tests in watch mode:
npm run test:watchGenerate coverage report:
npm run test:covChangelog
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
- Inspired by Laravel's likeable packages and overtrue/laravel-like
License
The MIT License (MIT). Please see License File for more information.
