NestboltNestbolt

@nestbolt/likeable

Mixin

Add like(), unlike(), toggle(), and query methods directly on your entity instances.

LikeableMixin adds convenience methods to your entity class so you can work with likes directly on entity instances, without injecting LikeableService.

Basic Usage

Extend your entity from LikeableMixin():

import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";
import { Likeable, LikeableMixin } from "@nestbolt/likeable";

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

  @Column()
  title!: string;
}

The base class passed to LikeableMixin() declares the shape that the mixin needs -- specifically that the entity has an id field. Use declare in the extending class to redeclare the TypeORM-decorated id column.

Methods

like(userId)

Records that userId likes this entity. Idempotent -- calling like() twice with the same user is a no-op:

const post = await repo.findOneByOrFail({ id: "..." });
await post.like(userId);
await post.like(userId); // no-op, no exception

unlike(userId)

Removes the like for userId:

await post.unlike(userId);

Calling unlike() for a user who hasn't liked the entity is a no-op and emits no event.

toggle(userId)

Flips the like state for userId and returns the new state:

const liked = await post.toggle(userId);
console.log(liked); // true if now liked, false if now unliked

isLikedBy(userId)

Returns true if userId has liked this entity:

const liked = await post.isLikedBy(userId);

getLikesCount()

Returns the total number of likes on this entity:

const count = await post.getLikesCount();

getLikers()

Returns the list of user IDs that have liked this entity, ordered by most recent first:

const userIds = await post.getLikers();

Composing with Other Mixins

LikeableMixin composes with the other Nestbolt mixins like HasMediaMixin and SluggableMixin:

import { HasMedia, HasMediaMixin } from "@nestbolt/medialibrary";
import { Sluggable, SluggableMixin } from "@nestbolt/sluggable";
import { Likeable, LikeableMixin } from "@nestbolt/likeable";

@Sluggable({ from: "name" })
@Likeable({ type: "Product" })
@HasMedia({ modelType: "Product" })
@Entity("products")
export class Product extends LikeableMixin(
  SluggableMixin(
    HasMediaMixin(class {
      id!: string;
    }),
  ),
) {
  @PrimaryGeneratedColumn("uuid")
  declare id: string;

  @Column()
  name!: string;

  @Column({ default: "" })
  slug!: string;
}

// Product now has all three mixin APIs:
// product.like(userId), product.toggle(userId), product.getLikesCount()
// product.getSlug(), product.findBySlug(), product.regenerateSlug()
// product.addMedia(), product.getMedia(), product.getFirstMediaUrl()

The order of mixin wrapping doesn't matter functionally -- pick whatever reads best for your codebase.

How the Bridge Works

The mixin doesn't depend on dependency injection. Instead, it calls LikeableService.getInstance() -- a static accessor that returns the active service instance after LikeableModule has been initialized.

This means:

  • The methods work on entity instances loaded via TypeORM, manually constructed, or returned from any source.
  • You don't need to pass the service in -- but you do need LikeableModule.forRoot() to be imported in your application module.

Error Handling

If LikeableModule has not been initialized (e.g., you call a mixin method before the Nest application has bootstrapped), a LikeableNotInitializedException is thrown:

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

try {
  await post.like(userId);
} catch (error) {
  if (error instanceof LikeableNotInitializedException) {
    console.error("LikeableModule has not been initialized");
  }
}

In practice, this should never fire in a running app -- it's a defensive guard for tests or scripts that run outside the normal bootstrap sequence.