Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/bknd-define-relationship" ~/.claude/skills/majiayu000-claude-skill-registry-bknd-define-relationship && rm -rf "$T"
skills/data/bknd-define-relationship/SKILL.mdDefine 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.,
createsauthor
)author_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:
→ Points to parent categorycategory.parent_id- 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 |
|---|---|---|---|
| string | Target entity name | FK field name (e.g., → ) |
| string | Source entity name | Reverse navigation name |
| boolean | false | Relationship is mandatory |
ManyToMany Options
| Option | Type | Default | Description |
|---|---|---|---|
| string | | 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:
- Restart server (schema syncs on startup)
- Verify relation is in second
argumentem() - 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
for semantic field names (mappedBy
instead ofauthor
)users - Define relations in the second
argumentem() - Use
for simple FK without navigation.references()
DON'T:
- Use singular entity names in relations
- Create manual FK fields when using
(it creates them automatically)relation() - Use
on one-to-one relations$set - Forget to destructure entities in the callback
Related Skills
- bknd-create-entity - Create entities before defining relationships
- bknd-add-field - Add fields including
for simple FKs.references() - bknd-crud-read - Query related data with
andwithjoin - bknd-crud-update - Use
,$attach
,$detach
for relation updates$set - bknd-query-filter - Advanced filtering on relations