Relations

Link entities with single, many-to-many, and polymorphic relations.

Relations connect entities to each other. CMS supports three types of relations, all defined with the same relation() helper.

Single relations

A single relation stores a foreign key column on the entity’s table.


          import { entity, text, relation } from "@cms/config";

const authors = entity("authors", {
  fields: [text("name", { required: true })],
});

const posts = entity("posts", {
  fields: [
    text("title", { required: true }),
    relation("author", { to: authors }),
  ],
});
        

This creates an author_id column on the posts table referencing authors.id.

In the API, an unresolved single relation returns just the ID:


          { "id": "...", "title": "My Post", "author": "author-uuid" }
        

Many-to-many relations

Set multiple: true to create a junction table.


          const posts = entity("posts", {
  fields: [
    text("title", { required: true }),
    relation("tags", { to: tags, multiple: true }),
  ],
});
        

This creates a posts_tags junction table with sourceId, targetId, and position columns. Position preserves insertion order. The database enforces referential integrity — deleting a tag automatically removes it from all posts.

Do not use array() to hold multiple references — wrapping a relation in an array stores IDs as JSONB, losing referential integrity, cross-entity filtering, and resolution. See Handling multiple relations.

Unresolved multiple relations return an array of IDs:


          { "id": "...", "title": "My Post", "tags": ["tag-1", "tag-2"] }
        

Polymorphic relations

Pass an array of entities to to for polymorphic relations:


          relation("link", { to: [pages, posts, externalLinks] })
        

The target entity type is stored alongside the ID.

Resolving relations

By default, relations return raw IDs. Use the resolve query parameter to expand them into full objects:


          GET /api/posts?resolve[author]=*
GET /api/posts?resolve[author]=name,bio
GET /api/posts?resolve[tags]=title
        

Resolved response:


          {
  "data": {
    "id": "...",
    "title": "My Post",
    "author": {
      "id": "author-uuid",
      "name": "Pedro",
      "bio": "Design engineer"
    }
  }
}
        

Nested resolution

You can resolve relations on resolved entities (max 2 levels deep):


          GET /api/posts?resolve[author]=name,avatar&resolve[author.avatar]=url,alt
        

Resolution behavior

  • resolve[field]=* — all fields on the related entity
  • resolve[field]=name,bio — only specified fields (id is always included)
  • Multiple relations are resolved via junction table JOINs, ordered by position
  • Resolution uses batched WHERE id = ANY(...) queries — never N+1
  • Unresolved relations on resolved entities return raw IDs unless explicitly resolved

Filtering across relations

You can filter by fields on related entities using dot notation:


          GET /api/posts?filter[author.name]=Pedro
        

This generates a subquery that joins to the related table. See Querying for more on filtering.

Previous

Fields

Next

Querying