Veap ships four optional packages that add common content capabilities to any @veap/database model:
| Package | Capability | Shared table |
|---|---|---|
@veap/translatable | Multilingual field values | translations |
@veap/taggable | Free-form tags | tags + taggables |
@veap/categorizable | Hierarchical categories | categories + categorizables |
@veap/commentable | Threaded comments/reviews | comments |
Each package is marked private in the monorepo and is consumed as a workspace dependency, not from the public registry.
Guides#
The shared design#
These packages all follow the same pattern:
- One shared table per capability. Translatable stores every model's translations in a single
translationstable; taggable and categorizable store the taxonomy intags/categoriesplus a polymorphic pivot; commentable stores every model's comments in onecommentstable. The owner is identified by a*_type/*_idpair (amorphMany,morphToManyormorphTorelation). - A mixin and a base class. You either extend the provided
*Modelbase class or wrap an existing class with the*()mixin — useful when your model already extends another base. - A scoped query builder. Each trait returns a
*QueryBuilderfromstatic query()so you getwhereTranslation,whereTag,whereCategoryandwhereHasComments-style helpers and eager loading for free. - A
MorphMapregistration. Each model registers its morph alias (tag,category,comment, …) so polymorphic relations can resolve the target class. Register the aliases your models use. - Migrations that do not drop shared tables. The
down()functions intentionally keep the shared tables, because independent packages share them; each owner cleans up its own rows. - Optional UI components. Each package exposes client components under
./uifor editors (a translatable form, a tag input, a category tree, a comment section).
Migrations#
Run the trait migrations through the host application's migration flow. See the migrations guide and register them with withMigrations([...]) or your own runner. The dependency order is:
translations (required by taggable and categorizable)
tags + taggables
categories + categorizables
commentsBecause the tables are shared, a plugin rollback keeps them; delete rows by owner instead.
Locale defaults#
The taggable and categorizable helpers default to the pl locale, and the locale manager in translatable defaults to en, while the translatable UI components default to ["pl", "en"]. These defaults differ across packages — set the locale explicitly whenever the value matters.
Security and correctness#
- A moderation
statuson a comment is data, not authorization. Enforce who may read, create or moderate content with server-side checks. - Guest-provided values (names, emails, comment bodies, tag/category names) are untrusted input: validate length, sanitize rendered output, and never interpolate them into raw SQL.
- These traits use the database layer's writes directly. If your application requires every write to run inside
transaction(), wrap the trait method calls yourself. - Slug helpers generate slugs from arbitrary text; enforce uniqueness in your own code if a duplicate slug would break routing.