@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:
- Fetches the entity's current tags
- Detaches tags not in the new list
- 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 falsegetTagCount(Entity, entityId)
Returns the number of tags attached to an entity:
const count = await taggableService.getTagCount(Post, postId);
// 3getEntitiesWithTag(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 IDsPivot Table
The taggables pivot table stores the relationships:
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
tag_id | varchar(36) | Foreign key to tags.id (cascades on delete) |
taggable_type | varchar(255) | Entity type (from @Taggable() or class name) |
taggable_id | varchar(36) | Entity ID |
created_at | timestamp | When the tag was attached |
A composite unique index on [tag_id, taggable_type, taggable_id] prevents duplicate attachments.