Define Entity Relationships
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.
SKILL.md
Full skill instructions
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
| Type | Use Case | Example |
|---|---|---|
| Many-to-One | Child belongs to one parent | Posts → User (author) |
| One-to-One | Exclusive 1:1 pairing | User → Profile |
| Many-to-Many | Both sides have multiple | Posts ↔ Tags |
| Self-Referencing | Entity references itself | Categories → 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
- Start server:
npx bknd run - Open
http://localhost:1337 - Navigate to Data section
Step 2: Add Relation Field
- Click on the child entity (e.g.,
posts) - Click + Add Field
- Select Relation field type
- Choose the target entity (e.g.,
users) - 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.,
authorcreatesauthor_id) - Required: Toggle if relationship is mandatory
Step 4: Save and Sync
- Click Save Field
- 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
| Option | Type | Default | Description |
|---|---|---|---|
mappedBy | string | Target entity name | FK field name (e.g., author → author_id) |
inversedBy | string | Source entity name | Reverse navigation name |
required | boolean | false | Relationship is mandatory |
ManyToMany Options
| Option | Type | Default | Description |
|---|---|---|---|
connectionTable | string | {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: "[email protected]" } },
},
});
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:
- Restart server (schema syncs on startup)
- Verify relation is in second
em()argument - 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: "[email protected]" });
// 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: "[email protected]" }
DOs and DON'Ts
DO:
- Use plural entity names (
users,posts) - Use
mappedByfor semantic field names (authorinstead ofusers) - 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
$seton 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
withandjoin - bknd-crud-update - Use
$attach,$detach,$setfor relation updates - bknd-query-filter - Advanced filtering on relations
