NestboltNestbolt

@nestbolt/taggable

Attaching Tags

Attach, detach, sync, and query tags on any entity.

Attaching a Tag

attachTag(Entity, entityId, tagId)

Attaches a tag to an entity. If the tag is already attached, the call is a no-op (no duplicate records are created):

const tag = await taggableService.findOrCreateTag("NestJS");
await taggableService.attachTag(Post, postId, tag.id);

The first argument is the entity class (constructor), not an instance. The service reads the @Taggable() metadata from the class to determine the taggableType.

Detaching a Tag

detachTag(Entity, entityId, tagId)

Removes a tag from an entity:

await taggableService.detachTag(Post, postId, tagId);

If the tag wasn't attached, the call is a no-op.

Syncing Tags

syncTags(Entity, entityId, tagIds)

Replaces all tags on an entity with the given set. Tags not in the list are detached, and tags in the list that aren't already attached are attached:

await taggableService.syncTags(Post, postId, [tagId1, tagId2, tagId3]);

This is the most efficient way to update an entity's tags in bulk. Internally it:

  1. Fetches the entity's current tags
  2. Detaches tags not in the new list
  3. Attaches tags that are new

Example: Categorize a Post

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),
  );
}

Querying Tags

getEntityTags(Entity, entityId)

Returns all tags attached to an entity:

const tags = await taggableService.getEntityTags(Post, postId);
// TagEntity[]

tags.forEach(tag => {
  console.log(`${tag.name} (${tag.slug})`);
});

hasTag(Entity, entityId, tagId)

Checks if a specific tag is attached to an entity:

const isTagged = await taggableService.hasTag(Post, postId, tagId);
// true or false

getTagCount(Entity, entityId)

Returns the number of tags attached to an entity:

const count = await taggableService.getTagCount(Post, postId);
// 3

getEntitiesWithTag(Entity, tagId)

Returns the IDs of all entities of a given type that have a specific tag:

const postIds = await taggableService.getEntitiesWithTag(Post, tagId);
// ["abc-123", "def-456", ...]

This is useful for filtering or listing entities by tag.

Polymorphic Behavior

Tags are scoped by entity type. The same tag can be attached to different entity types without interference:

const tag = await taggableService.findOrCreateTag("Featured");

// Attach to both Post and Product
await taggableService.attachTag(Post, postId, tag.id);
await taggableService.attachTag(Product, productId, tag.id);

// Queries are scoped by entity type
const postTags = await taggableService.getEntityTags(Post, postId);
// Returns the "Featured" tag

const productTags = await taggableService.getEntityTags(Product, productId);
// Also returns the "Featured" tag

// getEntitiesWithTag is also scoped
const featuredPosts = await taggableService.getEntitiesWithTag(Post, tag.id);
// Only returns Post IDs

const featuredProducts = await taggableService.getEntitiesWithTag(Product, tag.id);
// Only returns Product IDs

Pivot Table

The taggables pivot table stores the relationships:

ColumnTypeDescription
idUUIDPrimary key
tag_idvarchar(36)Foreign key to tags.id (cascades on delete)
taggable_typevarchar(255)Entity type (from @Taggable() or class name)
taggable_idvarchar(36)Entity ID
created_attimestampWhen the tag was attached

A composite unique index on [tag_id, taggable_type, taggable_id] prevents duplicate attachments.