Docs/Guides/ORM traits

Categorizable

@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…

2 min read

@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:

ts
import { runMigrations } from "@veap/database";
import { migrations as categorizableMigrations } from "@veap/categorizable/migrations";

await runMigrations("app", categorizableMigrations);

Make a model categorizable#

ts
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:

ts
import { Categorizable } from "@veap/categorizable";
import { TranslatableModel } from "@veap/translatable";

export class Article extends Categorizable(TranslatableModel) {
  static override table = "articles";
}

Assign, sync and detach#

ts
await post.assignCategory("Engineering");
await post.syncCategories(["Engineering", "News"]); // replace the whole set
await post.detachCategories("News");
await post.detachCategories();                       // detach all

String 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#

ts
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#

ts
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 pl locale; pass the locale explicitly when needed.
  • findOrCreateByName creates missing categories as a side effect; validate and curate input so the taxonomy does not accumulate near-duplicates.

See also#

veap
Preparing0%