Agent skill
entity-system
Config-driven entity system for creating automatic CRUDs. Includes: config, fields, types, service, messages (i18n), migrations. Use this skill to create, modify, or understand system entities.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/entity-system
SKILL.md
Entity System Skill
Config-driven system for defining entities with automatic CRUDs, similar to WordPress Custom Post Types.
Architecture Overview
┌──────────────────────────────────────────────────────────────────────────┐
│ ENTITY CONFIGURATION │
│ │
│ contents/themes/{theme}/entities/{entity}/ │
│ ├── {entity}.config.ts ← Main configuration │
│ ├── {entity}.fields.ts ← Field definitions │
│ ├── {entity}.types.ts ← TypeScript types │
│ ├── {entity}.service.ts ← Optional but recommended │
│ ├── messages/ │
│ │ ├── en.json ← English translations │
│ │ ├── es.json ← Spanish translations │
│ │ └── {locale}.json ← Additional locales if project requires │
│ └── migrations/ │
│ ├── 001_{entity}_table.sql │
│ ├── 002_{entity}_metas.sql (if metadata enabled) │
│ └── sample_data.json ← Sample data for seeding │
└──────────────────────────────────────────────────────────────────────────┘
📍 Context-Aware Paths: Entity configs go in
contents/themes/{theme}/entities/in both contexts. Core entities are read-only in consumer projects. Seecore-theme-responsibilitiesskill for complete rules.
When to Use This Skill
- Creating a new entity (CRUD resource)
- Adding/modifying fields on an entity
- Understanding entity configuration structure
- Generating migrations for entity tables
- Creating sample data for development
Entity Configuration Structure
EntityConfig (4 Sections)
interface EntityConfig {
// 1. BASIC IDENTIFICATION
slug: string // URL/table name (single source of truth)
enabled: boolean
names: { singular: string, plural: string }
icon: LucideIcon
// 2. ACCESS AND SCOPE
access: {
public: boolean // Accessible without auth
api: boolean // Has external API endpoints
metadata: boolean // Supports key-value metadata
shared: boolean // Shared among team members (no userId filter)
}
// 3. UI/UX FEATURES
ui: {
dashboard: {
showInMenu: boolean
showInTopbar: boolean
filters?: EntityFilterConfig[]
}
public: {
hasArchivePage: boolean
hasSinglePage: boolean
}
features: {
searchable: boolean
sortable: boolean
filterable: boolean
bulkOperations: boolean
importExport: boolean
}
}
// 4. INTERNATIONALIZATION
i18n: {
fallbackLocale: 'en' | 'es'
loaders: Record<'en' | 'es', () => Promise<object>>
}
// FIELDS (imported separately)
fields: EntityField[]
}
Note: Permissions are defined centrally in
config/permissions.config.ts, not in entity config.
Field Types
type EntityFieldType =
// Basic
| 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'datetime'
| 'email' | 'url' | 'json'
// Selection
| 'select' | 'multiselect' | 'radio' | 'buttongroup' | 'tags' | 'combobox'
// Media
| 'file' | 'image' | 'video' | 'audio' | 'media-library'
// Specialized
| 'phone' | 'rating' | 'range' | 'doublerange'
| 'markdown' | 'richtext' | 'code'
| 'timezone' | 'currency' | 'country' | 'address'
// Relations
| 'relation' | 'relation-multi' | 'relation-prop' | 'relation-prop-multi'
| 'reference' | 'user'
Field Definition
interface EntityField {
name: string
type: EntityFieldType
required: boolean
defaultValue?: unknown
validation?: ZodSchema
display: {
label: string
description?: string
placeholder?: string
showInList: boolean
showInDetail: boolean
showInForm: boolean
order: number
columnWidth?: number // 1-12 grid
}
api: {
searchable: boolean
sortable: boolean
filterable?: boolean
readOnly: boolean
}
// For select/multiselect
options?: { value: string, label: string }[]
// For relations
relation?: {
entity: string
titleField?: string
parentId?: string
userFiltered?: boolean
}
}
Auto-Generated Features
From an EntityConfig, the system automatically provides:
- Database Table - Via migrations
- API Endpoints -
/api/v1/{slug}with CRUD operations - Dashboard UI - List, create, edit, delete views
- Form Components - Auto-generated from fields
- Validation - Server and client-side from field config
- i18n Support - Labels, placeholders, messages
- Search & Filtering - Based on field API config
- Metadata System - Key-value pairs (if enabled)
- Server Actions - Direct CRUD from Client Components
Server Actions (Client Component CRUD)
Server Actions permiten ejecutar operaciones CRUD desde Client Components sin pasar por HTTP.
Server Actions Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ SERVER ACTIONS FLOW │
│ │
│ Client Component Server Action GenericEntityService
│ ───────────────── ───────────── ────────────────────
│ │ │ │
│ │ createEntity(slug,data) │ │
│ │ ─────────────────────► │ │
│ │ │ │
│ │ ┌─────┴─────┐ │
│ │ │ 1. Auth │ │
│ │ │ (session) │ │
│ │ ├───────────┤ │
│ │ │ 2. Perms │ │
│ │ │ (registry)│ │
│ │ └─────┬─────┘ │
│ │ │ │
│ │ │ create(slug,uid,tid,data)
│ │ │ ─────────────────────► │
│ │ │ ┌────┴────┐
│ │ │ │ Validate│
│ │ │ │ Hooks │
│ │ │ │ SQL+RLS │
│ │ │ └────┬────┘
│ │ │ ◄───────────────────── │
│ │ ◄───────────────────── │ │
│ │ EntityActionResult │ │
└─────────────────────────────────────────────────────────────────────┘
Server Actions vs API HTTP
| Escenario | Server Actions | API HTTP |
|---|---|---|
| Client Component mutations | Recommended | No |
| Server Component data fetching | No | Yes |
| External integrations | No | Yes |
| Webhooks | No | Yes |
| Cache revalidation automática | Yes | Manual |
Available Functions
| Function | Required Permission | Description |
|---|---|---|
createEntity(slug, data, config?) |
{slug}.create |
Create entity |
updateEntity(slug, id, data, config?) |
{slug}.update |
Update entity |
deleteEntity(slug, id, config?) |
{slug}.delete |
Delete one |
deleteEntities(slug, ids, config?) |
{slug}.delete |
Delete many |
getEntity(slug, id) |
{slug}.read |
Get by ID |
listEntities(slug, options?) |
{slug}.list |
List with filters |
entityExists(slug, id) |
{slug}.read |
Check existence |
countEntities(slug, where?) |
{slug}.list |
Count records |
Usage Example
'use client'
import { createEntity, updateEntity, deleteEntity } from '@nextsparkjs/core/actions'
// CREATE - Auth and permissions verified automatically
async function handleCreate(data: FormData) {
const result = await createEntity('schools', {
name: data.get('name'),
status: 'active'
})
if (result.success) {
console.log('Created:', result.data)
} else {
console.error('Error:', result.error)
}
}
// UPDATE with custom revalidation
async function handleUpdate(id: string, data: Partial<School>) {
const result = await updateEntity('schools', id, data, {
revalidatePaths: ['/dashboard/overview'],
revalidateTags: ['school-stats'],
})
}
// DELETE with redirect
async function handleDelete(id: string) {
await deleteEntity('schools', id, {
redirectTo: '/dashboard/schools'
})
}
Return Types
// For operations that return data
type EntityActionResult<T> =
| { success: true; data: T }
| { success: false; error: string }
// For void operations (delete)
type EntityActionVoidResult =
| { success: true }
| { success: false; error: string }
// List result
interface ListEntityResult<T> {
data: T[]
total: number
limit: number
offset: number
}
Action Configuration
interface ActionConfig {
revalidatePaths?: string[] // Paths to revalidate
revalidateTags?: string[] // Cache tags
redirectTo?: string // Redirect after action
}
Automatic Security
- Auth: userId from
getTypedSession()(server-side) - Team: teamId from httpOnly cookie
activeTeamId - Permissions: Verified against
permissions.config.ts - Validation: Fields validated against entity schema
- RLS: Queries executed with Row-Level Security
- Team Isolation: All operations filter by active teamId (prevents cross-team access for multi-team users)
Team Isolation (Multi-Team Users)
Users can belong to multiple teams. Server Actions automatically filter by the active team cookie to prevent accidental cross-team data access:
User belongs to: TeamA, TeamB
Active team cookie: TeamA
getEntity('campaigns', 'id-from-teamB') → Returns null (filtered out)
updateEntity('campaigns', 'id-from-teamB', data) → Error: not found
deleteEntity('campaigns', 'id-from-teamB') → Error: not found
This isolation is automatic and cannot be bypassed from client code.
Server Actions Anti-Patterns
// NEVER: Ignore the result
await createEntity('schools', data) // Without checking success
// CORRECT: Always check result
const result = await createEntity('schools', data)
if (!result.success) {
toast.error(result.error)
}
// NEVER: Use for Server Components (no 'use client')
// Server Actions are for CLIENT Components only
// CORRECT: In Server Components use service directly
import { GenericEntityService } from '@nextsparkjs/core/services'
const data = await GenericEntityService.list('schools', userId, { teamId })
List Entities Example
'use client'
import { listEntities } from '@nextsparkjs/core/actions'
async function loadCampaigns() {
const result = await listEntities<Campaign>('campaigns', {
where: { status: 'active' },
orderBy: 'createdAt',
orderDir: 'desc',
limit: 20,
offset: 0,
search: 'marketing', // Full-text search on searchable fields
})
if (result.success) {
const { data, total, limit, offset } = result.data
console.log(`Showing ${data.length} of ${total} campaigns`)
}
}
Server Actions Files
| File | Purpose |
|---|---|
core/lib/actions/entity.actions.ts |
Server Actions (entry points) |
core/lib/actions/types.ts |
TypeScript types |
core/lib/services/generic-entity.service.ts |
Business logic |
core/lib/permissions/check.ts |
Permission verification |
Metadata System
When access.metadata: true is set in the entity config, the entity supports dynamic key-value metadata.
Migration Required
A separate migration creates the {entity}_metas table:
-- migrations/002_{entity}_metas.sql
CREATE TABLE IF NOT EXISTS "{entity}_metas" (
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
"{entity}Id" TEXT NOT NULL REFERENCES "{entity}"(id) ON DELETE CASCADE,
"metaKey" TEXT NOT NULL,
"metaValue" JSONB NOT NULL DEFAULT '{}'::jsonb,
"dataType" TEXT, -- Optional: string, number, boolean, json
"isPublic" BOOLEAN NOT NULL DEFAULT FALSE, -- Visible without auth
"isSearchable" BOOLEAN NOT NULL DEFAULT FALSE, -- Indexed for search
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT {entity}_metas_unique_key UNIQUE ("{entity}Id", "metaKey")
);
-- Indexes
CREATE INDEX "idx_{entity}_metas_entity_id" ON "{entity}_metas"("{entity}Id");
CREATE INDEX "idx_{entity}_metas_key" ON "{entity}_metas"("metaKey");
CREATE INDEX "idx_{entity}_metas_value_gin" ON "{entity}_metas" USING GIN ("metaValue");
When to Use Metas vs Fields
| Use Case | Fields | Metas |
|---|---|---|
| Structured, validated data | ✅ | ❌ |
| Shown in forms/lists | ✅ | ❌ |
| Searchable/sortable | ✅ | ❌ |
| Dynamic/extensible data | ❌ | ✅ |
| Plugin-specific data | ❌ | ✅ |
| User preferences/settings | ❌ | ✅ |
| Third-party integrations | ❌ | ✅ |
API Access
Metas are accessed via query parameter (covered in entity-api skill):
GET /api/v1/products/123?metas=all
GET /api/v1/products/123?metas=category,tags
Child Entities
Child entities are 1:N relationships where the child only exists in the context of its parent (e.g., order items, post comments).
Configuration in EntityConfig
export const orderEntityConfig: EntityConfig = {
slug: 'orders',
// ... other config
childEntities: {
'items': {
table: 'order_items', // Convention: {parent}_{child}
showInParentView: true, // Show in parent detail view
hasOwnRoutes: false, // Only via /orders/{id}/child/items
fields: [
{
name: 'productName',
type: 'text',
required: true,
display: { label: 'Product' }
},
{
name: 'quantity',
type: 'number',
required: true,
display: { label: 'Qty' }
}
],
display: {
title: 'Order Items',
description: 'Products in this order',
mode: 'table' // 'table' | 'cards' | 'list'
}
}
}
}
ChildEntityDefinition Interface
interface ChildEntityDefinition {
table: string // Database table name
fields: ChildEntityField[] // Child entity fields
showInParentView: boolean // Show in parent view
hasOwnRoutes: boolean // Has independent routes?
// Note: Permissions are defined centrally in permissions.config.ts
display: {
title: string
description?: string
mode: 'table' | 'cards' | 'list'
}
}
Migration Required
-- migrations/002_{parent}_{child}.sql
CREATE TABLE "order_items" (
"id" TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
"parentId" TEXT NOT NULL REFERENCES "orders"(id) ON DELETE CASCADE,
"productName" VARCHAR(255) NOT NULL,
"quantity" INTEGER NOT NULL,
"createdAt" TIMESTAMPTZ NOT NULL DEFAULT now(),
"updatedAt" TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX "idx_order_items_parentId" ON "order_items"("parentId");
-- RLS: Access via parent ownership
ALTER TABLE "order_items" ENABLE ROW LEVEL SECURITY;
CREATE POLICY "order_items_via_parent" ON "order_items"
FOR ALL TO authenticated
USING (
EXISTS (
SELECT 1 FROM "orders"
WHERE "orders".id = "parentId"
AND "orders"."userId" = public.get_auth_user_id()
)
);
Key Properties
| Property | Required | Description |
|---|---|---|
table |
Yes | Table name. Convention: {parent}_{child} |
fields |
Yes | Child entity fields (same structure as entity fields) |
showInParentView |
Yes | Display in parent detail view |
hasOwnRoutes |
Yes | If true: also /api/v1/{child}. If false: only via parent |
display.mode |
Yes | table (rows), cards (cards), list (simple) |
Limitations
- One level deep: Child entities cannot have their own children
- Cascade deletion: Deleting parent deletes all children
- No public routes: Children don't have public pages
API Access
Child entity endpoints (covered in entity-api skill):
GET /api/v1/orders/{id}/child/items
POST /api/v1/orders/{id}/child/items
GET /api/v1/orders/{id}/child/items/{itemId}
PATCH /api/v1/orders/{id}/child/items/{itemId}
DELETE /api/v1/orders/{id}/child/items/{itemId}
Scripts
Scaffold New Entity
python .claude/skills/entity-system/scripts/scaffold-entity.py --entity products --theme default
Generate Migration
python .claude/skills/entity-system/scripts/generate-migration.py --entity products --theme default
Generate Metas Migration
python .claude/skills/entity-system/scripts/generate-metas-migration.py --entity products --theme default
Generate Child Entity Migration
python .claude/skills/entity-system/scripts/generate-child-migration.py --parent orders --child items --theme default
Generate Sample Data
python .claude/skills/entity-system/scripts/generate-sample-data.py --entity products --count 10
Key Files Reference
| File | Purpose |
|---|---|
core/lib/entities/types.ts |
All entity type definitions |
core/lib/entities/migration-helper.ts |
Migration SQL generation |
core/lib/entities/registry.ts |
Entity registry (auto-generated) |
core/lib/entities/queries.ts |
Database query helpers |
core/lib/entities/helpers.ts |
Utility functions |
Naming Conventions
- Slug: kebab-case, plural (
tasks,blog-posts) - Table: Same as slug (
tasks,blog_posts) - API Path:
/api/v1/{slug} - Config file:
{singular}.config.ts(task.config.ts) - Fields file:
{slug}.fields.ts(tasks.fields.ts)
Anti-Patterns
// ❌ NEVER: Add system fields manually
fields: [
{ name: 'id', type: 'text', ... }, // Auto-included
{ name: 'teamId', type: 'text', ... }, // Auto-included (team-mode entities)
{ name: 'createdAt', type: 'datetime' }, // Auto-included
{ name: 'updatedAt', type: 'datetime' }, // Auto-included
]
// ❌ NEVER: Use dynamic imports for entity configs
const config = await import(`@/contents/entities/${slug}`)
// ❌ NEVER: Define tableName explicitly (derived from slug)
tableName: 'my_custom_table'
// ✅ CORRECT: Only declare business fields
fields: [
{ name: 'title', type: 'text', ... },
{ name: 'status', type: 'select', ... },
]
Checklist for New Entity
- Created
{entity}.config.tswith all 5 sections - Created
{entity}.fields.tswith field definitions - Created
{entity}.types.tswith TypeScript interfaces - Created
messages/with required locales (en.json, es.json, + others if project defines) - Created migration in
migrations/001_{entity}_table.sql - Created
_metasmigration ifaccess.metadata: true - Added entity to theme's entity registry
- Ran
node core/scripts/build/registry.mjs
References
- Load
references/entity-types.mdfor complete type definitions - Load
references/field-examples.mdfor field configuration examples - Load
references/migration-patterns.mdfor SQL patterns
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?