bknd-define-relationship

Use when defining relationships between Bknd entities. Covers many-to-one, one-to-one, many-to-many, self-referencing relationships, junction tables, options like mappedBy and inversedBy, and UI vs code approaches.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "bknd-define-relationship" with this command: npx skills add cameronapak/bknd-skills/cameronapak-bknd-skills-bknd-define-relationship

Define Entity Relationships

Create relationships between entities in Bknd (foreign keys, references, associations).

Prerequisites

  • At least two entities exist (see bknd-create-entity)
  • For code mode: Access to your schema file

Relationship Types

TypeUse CaseExample
Many-to-OneChild belongs to one parentPosts → User (author)
One-to-OneExclusive 1:1 pairingUser → Profile
Many-to-ManyBoth sides have multiplePosts ↔ Tags
Self-ReferencingEntity references itselfCategories → Parent Category

When to Use UI vs Code

Use UI Mode When

  • Quick prototyping
  • Visual learners
  • Non-developers setting up relationships

Use Code Mode When

  • Version control needed
  • Reproducible schema
  • Custom options (mappedBy, connectionTable)
  • Team collaboration

UI Approach

Step 1: Access Data Section

  1. Start server: npx bknd run
  2. Open http://localhost:1337
  3. Navigate to Data section

Step 2: Add Relation Field

  1. Click on the child entity (e.g., posts)
  2. Click + Add Field
  3. Select Relation field type
  4. Choose the target entity (e.g., users)
  5. Select relationship type:
    • Many-to-One: Multiple posts can belong to one user
    • One-to-One: One post has exactly one user
    • Many-to-Many: Posts can have many tags, tags can have many posts

Step 3: Configure Options

  • Field Name: Name for the foreign key (e.g., author creates author_id)
  • Required: Toggle if relationship is mandatory

Step 4: Save and Sync

  1. Click Save Field
  2. Click Sync Database to apply changes

Code Approach

Relationships are defined in the second argument to em():

const schema = em(
  {
    // Entity definitions (first argument)
  },
  ({ relation, index }, entities) => {
    // Relationship definitions (second argument)
  }
);

Many-to-One

Child belongs to one parent. Most common relationship type.

import { em, entity, text } from "bknd";

const schema = em(
  {
    users: entity("users", { email: text().required() }),
    posts: entity("posts", { title: text().required() }),
  },
  ({ relation }, { users, posts }) => {
    relation(posts).manyToOne(users);
  }
);

Auto-generated: users_id foreign key column on posts table

Custom field name with mappedBy:

({ relation }, { users, posts }) => {
  relation(posts).manyToOne(users, {
    mappedBy: "author",  // Creates author_id instead of users_id
  });
}

One-to-One

Exclusive 1:1 relationship. Each child belongs to exactly one parent.

const schema = em(
  {
    users: entity("users", { email: text().required() }),
    profiles: entity("profiles", { bio: text() }),
  },
  ({ relation }, { users, profiles }) => {
    relation(profiles).oneToOne(users);
  }
);

Note: One-to-one relationships cannot use $set operator (maintains exclusivity).

Many-to-Many

Both entities can have multiple of the other. Junction table created automatically.

const schema = em(
  {
    posts: entity("posts", { title: text().required() }),
    tags: entity("tags", { name: text().required() }),
  },
  ({ relation }, { posts, tags }) => {
    relation(posts).manyToMany(tags);
  }
);

Auto-generated: posts_tags junction table with posts_id and tags_id columns

Custom junction table name:

({ relation }, { posts, tags }) => {
  relation(posts).manyToMany(tags, {
    connectionTable: "post_tags",  // Custom junction table name
  });
}

Extra fields on junction table:

({ relation }, { users, courses }) => {
  relation(users).manyToMany(courses, {
    connectionTable: "enrollments",
  }, {
    // Extra fields on junction table
    enrolled_at: date(),
    completed: boolean(),
    grade: number(),
  });
}

Self-Referencing

Entity references itself. Common for hierarchies (categories, comments, org charts).

const schema = em(
  {
    categories: entity("categories", { name: text().required() }),
  },
  ({ relation }, { categories }) => {
    relation(categories).manyToOne(categories, {
      mappedBy: "parent",      // FK field: parent_id
      inversedBy: "children",  // Reverse navigation
    });
  }
);

Usage:

  • category.parent_id → Points to parent category
  • Query children: api.data.readMany("categories", { where: { parent_id: 5 } })

Alternative: Direct Foreign Key

Instead of relation(), use .references() on a number field:

const schema = em({
  users: entity("users", { email: text().required() }),
  posts: entity("posts", {
    title: text().required(),
    author_id: number().references("users.id"),
  }),
});

Difference: .references() is simpler but doesn't create inverse navigation or support many-to-many.

Relation Options

ManyToOne / OneToOne Options

OptionTypeDefaultDescription
mappedBystringTarget entity nameFK field name (e.g., authorauthor_id)
inversedBystringSource entity nameReverse navigation name
requiredbooleanfalseRelationship is mandatory

ManyToMany Options

OptionTypeDefaultDescription
connectionTablestring{source}_{target}Junction table name

Querying Relations

Load Related Data (with)

const api = app.getApi();

// Load posts with their author
const posts = await api.data.readMany("posts", {
  with: {
    users: { select: ["email", "name"] },
  },
});
// Result: [{ id: 1, title: "...", users: { email: "...", name: "..." } }]

Filter by Relation

// Posts by specific author
const posts = await api.data.readMany("posts", {
  where: { author_id: 5 },
});

// Using join for complex filters
const posts = await api.data.readMany("posts", {
  join: {
    users: { where: { email: "john@example.com" } },
  },
});

Many-to-Many Operations

// Attach tags to post
await api.data.updateOne("posts", 1, {
  tags: { $attach: [1, 2, 3] },  // Tag IDs
});

// Detach tags
await api.data.updateOne("posts", 1, {
  tags: { $detach: [2] },
});

// Replace all tags
await api.data.updateOne("posts", 1, {
  tags: { $set: [4, 5] },
});

Many-to-One Operations

// Set author on post
await api.data.updateOne("posts", 1, {
  users: { $set: 5 },  // User ID
});

Common Patterns

Blog with Authors and Tags

const schema = em(
  {
    users: entity("users", {
      email: text().required().unique(),
      name: text(),
    }),
    posts: entity("posts", {
      title: text().required(),
      content: text(),
      published: boolean(),
    }),
    tags: entity("tags", {
      name: text().required().unique(),
    }),
  },
  ({ relation }, { users, posts, tags }) => {
    // Post has one author
    relation(posts).manyToOne(users, { mappedBy: "author" });

    // Posts have many tags
    relation(posts).manyToMany(tags);
  }
);

E-commerce Orders

const schema = em(
  {
    customers: entity("customers", { email: text().required() }),
    orders: entity("orders", { total: number() }),
    products: entity("products", { name: text().required(), price: number() }),
  },
  ({ relation }, { customers, orders, products }) => {
    // Order belongs to customer
    relation(orders).manyToOne(customers);

    // Order has many products (with quantity)
    relation(orders).manyToMany(products, {
      connectionTable: "order_items",
    }, {
      quantity: number().required(),
      unit_price: number().required(),
    });
  }
);

Nested Categories

const schema = em(
  {
    categories: entity("categories", {
      name: text().required(),
      slug: text().required().unique(),
    }),
  },
  ({ relation }, { categories }) => {
    relation(categories).manyToOne(categories, {
      mappedBy: "parent",
      inversedBy: "children",
    });
  }
);

// Usage: Get all children of category 5
const children = await api.data.readMany("categories", {
  where: { parent_id: 5 },
});

Common Pitfalls

Entity Not Found

Error: Entity "user" not found

Fix: Entity names are plural by convention. Use users not user.

// Wrong
relation(posts).manyToOne(user);

// Correct
relation(posts).manyToOne(users);

Circular Reference Error

Error: Circular dependency detected

Fix: For self-referencing, use proper options:

// Correct self-reference
relation(categories).manyToOne(categories, {
  mappedBy: "parent",
  inversedBy: "children",
});

Foreign Key Naming Conflict

Error: Field "users_id" already exists

Fix: Use mappedBy to specify a different field name:

// If you already have users_id, use a different name
relation(posts).manyToOne(users, { mappedBy: "author" });  // Creates author_id

Many-to-Many $set on One-to-One

Error: Cannot use $set on one-to-one relation

Fix: One-to-one maintains exclusivity differently. Use $create instead:

// For one-to-one
await api.data.updateOne("users", 1, {
  profiles: { $create: { bio: "Hello" } },
});

Missing Entity in Destructure

Error: Cannot read property 'manyToOne' of undefined

Fix: Ensure entity is destructured from second callback parameter:

// Wrong - missing users in destructure
({ relation }, { posts }) => {
  relation(posts).manyToOne(users);  // users is undefined
}

// Correct
({ relation }, { users, posts }) => {
  relation(posts).manyToOne(users);
}

Relation Changes Not Applying

Problem: Added relation but not seeing FK column.

Fixes:

  1. Restart server (schema syncs on startup)
  2. Verify relation is in second em() argument
  3. Check for syntax errors

Verification

Check Foreign Key Created

npx bknd debug paths
# Look for the FK field in entity output

Test Relation in Code

const api = app.getApi();

// Create parent
const user = await api.data.createOne("users", { email: "test@example.com" });

// Create child with relation
const post = await api.data.createOne("posts", {
  title: "Test Post",
  author_id: user.data.id,
});

// Load with relation
const loaded = await api.data.readOne("posts", post.data.id, {
  with: { users: true },
});
console.log(loaded.data.users);  // { id: 1, email: "test@example.com" }

DOs and DON'Ts

DO:

  • Use plural entity names (users, posts)
  • Use mappedBy for semantic field names (author instead of users)
  • Define relations in the second em() argument
  • Use .references() for simple FK without navigation

DON'T:

  • Use singular entity names in relations
  • Create manual FK fields when using relation() (it creates them automatically)
  • Use $set on one-to-one relations
  • Forget to destructure entities in the callback

Related Skills

  • bknd-create-entity - Create entities before defining relationships
  • bknd-add-field - Add fields including .references() for simple FKs
  • bknd-crud-read - Query related data with with and join
  • bknd-crud-update - Use $attach, $detach, $set for relation updates
  • bknd-query-filter - Advanced filtering on relations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

btca-bknd-repo-learn

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-login-flow

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-file-upload

No summary provided by upstream source.

Repository SourceNeeds Review
General

bknd-registration

No summary provided by upstream source.

Repository SourceNeeds Review