Skip to content

nestbolt/taggable

Repository files navigation

@nestbolt/taggable

Polymorphic tagging system for NestJS with TypeORM — attach tags and categories to any entity.

npm version npm downloads tests license


This package provides a polymorphic tagging system for NestJS that lets you attach tags and categories to any entity using a shared tags table and a taggables pivot table.

Once installed, using it is as simple as:

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

// Attach, detach, sync tags
const tag = await taggableService.findOrCreateTag("NestJS");
await taggableService.attachTag(Post, postId, tag.id);

Table of Contents

Installation

Install the package via npm:

npm install @nestbolt/taggable

Or via yarn:

yarn add @nestbolt/taggable

Or via pnpm:

pnpm add @nestbolt/taggable

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 tag.created, tag.attached events

Quick Start

1. Register the module in your AppModule

import { TaggableModule } from "@nestbolt/taggable";

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

2. Mark entities as taggable

import { Taggable, TaggableMixin } from "@nestbolt/taggable";

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

3. Use the service to manage tags

import { TaggableService } from "@nestbolt/taggable";

@Injectable()
export class PostService {
  constructor(private readonly taggableService: TaggableService) {}

  async tagPost(postId: string) {
    const tag = await this.taggableService.findOrCreateTag("NestJS");
    await this.taggableService.attachTag(Post, postId, tag.id);
  }
}

Module Configuration

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

Static Configuration (forRoot)

TaggableModule.forRoot();

Async Configuration (forRootAsync)

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

Using the @Taggable() Decorator

The @Taggable() class decorator marks an entity for polymorphic tagging:

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

Tag CRUD

// Create a tag
const tag = await taggableService.createTag("JavaScript", { type: "language" });

// Create with metadata
const tag = await taggableService.createTag("VIP", {
  metadata: { color: "gold" },
});

// Find or create (by slug)
const tag = await taggableService.findOrCreateTag("TypeScript");

// Find
const tag = await taggableService.findTagById(id);
const tag = await taggableService.findTagBySlug("javascript");
const tags = await taggableService.findTagsByType("category");
const all = await taggableService.getAllTags();

// Delete (also removes all pivot records)
await taggableService.deleteTag(tagId);

Attaching Tags

// Attach a tag to an entity
await taggableService.attachTag(Post, postId, tagId);

// Detach a tag
await taggableService.detachTag(Post, postId, tagId);

// Sync tags (adds missing, removes extra)
await taggableService.syncTags(Post, postId, [tagId1, tagId2]);

// Query
const tags = await taggableService.getEntityTags(Post, postId);
const hasTag = await taggableService.hasTag(Post, postId, tagId);
const count = await taggableService.getTagCount(Post, postId);
const entityIds = await taggableService.getEntitiesWithTag(Post, tagId);

Entity Mixin

The TaggableMixin adds convenience methods directly on your entity:

@Entity("posts")
@Taggable()
export class Post extends TaggableMixin(BaseEntity) {
  // ...
}

// Usage
const post = await postRepo.findOneBy({ id });
await post.attachTag(tagId);
await post.detachTag(tagId);
await post.syncTags([tagId1, tagId2]);
const tags = await post.getTags();
const hasTag = await post.hasTag(tagId);
const count = await post.getTagCount();
Method Returns Description
attachTag(tagId) Promise<void> Attach a tag
detachTag(tagId) Promise<void> Detach a tag
syncTags(tagIds) Promise<void> Sync tags
getTags() Promise<TagEntity[]> Get all tags
hasTag(tagId) Promise<boolean> Check if entity has tag
getTagCount() Promise<number> Count tags
getTaggableType() string Get the entity type name
getTaggableId() string Get the entity ID

Events

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

Event Payload When
tag.created { tag } After a tag is created
tag.deleted { tagId, tagName } After a tag is deleted
tag.attached { tag, taggableType, taggableId } After a tag is attached
tag.detached { tagId, taggableType, taggableId } After a tag is detached
import { TAGGABLE_EVENTS, TagAttachedEvent } from "@nestbolt/taggable";
import { OnEvent } from "@nestjs/event-emitter";

@OnEvent(TAGGABLE_EVENTS.TAG_ATTACHED)
handleTagAttached(event: TagAttachedEvent) {
  console.log(`Tag attached to ${event.taggableType}#${event.taggableId}`);
}

Using the Service Directly

Inject TaggableService for tag management and querying:

import { TaggableService } from "@nestbolt/taggable";

@Injectable()
export class PostService {
  constructor(private readonly taggableService: TaggableService) {}

  async categorizePost(postId: string, categoryNames: string[]) {
    const tags = await Promise.all(
      categoryNames.map((name) =>
        this.taggableService.findOrCreateTag(name, { type: "category" }),
      ),
    );
    await this.taggableService.syncTags(
      Post,
      postId,
      tags.map((t) => t.id),
    );
  }
}
Method Returns Description
createTag(name, opts?) Promise<TagEntity> Create a new tag
findOrCreateTag(name, opts?) Promise<TagEntity> Find by slug or create
findTagById(id) Promise<TagEntity | null> Find tag by ID
findTagBySlug(slug, type?) Promise<TagEntity | null> Find tag by slug
findTagsByType(type) Promise<TagEntity[]> Get all tags of a type
getAllTags() Promise<TagEntity[]> Get all tags
deleteTag(id) Promise<void> Delete tag and pivots
attachTag(Entity, entityId, tagId) Promise<void> Attach tag to entity
detachTag(Entity, entityId, tagId) Promise<void> Detach tag from entity
syncTags(Entity, entityId, tagIds) Promise<void> Sync entity tags
getEntityTags(Entity, entityId) Promise<TagEntity[]> Get entity's tags
hasTag(Entity, entityId, tagId) Promise<boolean> Check if entity has tag
getEntitiesWithTag(Entity, tagId) Promise<string[]> Get entity IDs with tag
getTagCount(Entity, entityId) Promise<number> Count entity's tags
getOptions() TaggableModuleOptions Get module options

Tag Entity

The tags table stores:

Column Type Description
id UUID Primary key
name varchar(255) Tag name
slug varchar(255) URL-friendly slug
type varchar(100) Tag type/group (nullable)
metadata text JSON metadata (nullable)
created_at timestamp Creation timestamp
updated_at timestamp Last update timestamp

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 tagging system for NestJS with TypeORM — attach tags and categories to any entity.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Contributors