@veap/categorizable adds hierarchical, polymorphic categorization to any model. Categories live in a shared categories table (with a parent_id hierarchy) and are attached through a categorizables pivot.
Install and migrate#
Run the @veap/translatable migrations first (category names/slugs are translated), then categorizable:
import { runMigrations } from "@veap/database";
import { migrations as categorizableMigrations } from "@veap/categorizable/migrations";
await runMigrations("app", categorizableMigrations);Make a model categorizable#
import { CategorizableModel } from "@veap/categorizable";
export class BlogPost extends CategorizableModel {
static override table = "blog_posts";
}Or via the mixin when the model already has a base class:
import { Categorizable } from "@veap/categorizable";
import { TranslatableModel } from "@veap/translatable";
export class Article extends Categorizable(TranslatableModel) {
static override table = "articles";
}Assign, sync and detach#
await post.assignCategory("Engineering");
await post.syncCategories(["Engineering", "News"]); // replace the whole set
await post.detachCategories("News");
await post.detachCategories(); // detach allString arguments are resolved by Category.findOrCreateByName (matching translated name or slug, optionally scoped to a parent); UUID-looking strings are treated as existing category ids.
Read categories#
const categories = await post.categories().get(); // all attached categories
const primary = await post.category(); // first attached category (or null)category() returns the first match from the underlying morphToMany; do not rely on it for a stable "primary" category unless you control the attach order.
Query by category#
const inEngineering = await BlogPost.query().whereCategory("Engineering", "en").get();
const byId = await BlogPost.query().whereCategory("550e8400-e29b-41d4-a716-446655440000").get();
const anyOf = await BlogPost.query().whereInCategories(["Engineering", "News"], "en").get();
const withCategories = await BlogPost.query().withCategories().get();Hierarchy#
Category exposes parent() and children() relations over parent_id. Build a tree by loading the rows and nesting them by parent_id, then render it with the UI component.
UI#
@veap/categorizable/ui exports CategoryTreeSelect, a client tree selector. Pass a nested CategoryItem[] as categories; value/onChange use category ids. Set multiple for multi-selection.
Caveats#
- The helpers default to the
pllocale; pass the locale explicitly when needed. findOrCreateByNamecreates missing categories as a side effect; validate and curate input so the taxonomy does not accumulate near-duplicates.