M2M Relationships
M2M Relationships
This guide covers how M2M relationships work in the auto-admin interface.
Overview
The admin module automatically displays M2M relationships from your Drizzle schema. Relations appear as multi-select fields in edit forms with zero or minimal configuration.
📝 Note: M2M relations are auto-detected from Drizzle FK references. See the API M2M Guide for how auto-detection works.
Quick Start
1. Define Drizzle Schema
// modules/blog/schema.ts
import { relations } from 'drizzle-orm'
export const articleCategories = sqliteTable('article_categories', {
articleId: integer('article_id')
.references(() => articles.id, { onDelete: 'cascade' }),
categoryId: integer('category_id')
.references(() => categories.id, { onDelete: 'cascade' }),
}, (table) => ({
pk: primaryKey({ columns: [table.articleId, table.categoryId] })
}))
// Define relations
export const articleCategoriesRelations = relations(articleCategories, ({ one }) => ({
article: one(articles, {
fields: [articleCategories.articleId],
references: [articles.id],
}),
category: one(categories, {
fields: [articleCategories.categoryId],
references: [categories.id],
}),
}))
2. Optional: Customize Labels
// nuxt.config.ts
export default defineNuxtConfig({
autoApi: {
m2m: {
// Auto-detection enabled by default
relations: {
articles: {
categories: {
label: 'Categories', // ← Custom label for admin UI
help: 'Select categories for this article', // ← Help text
displayField: 'name', // ← Field shown in dropdown
}
}
}
}
}
})
3. That's It!
The admin UI automatically:
- ✅ Detects M2M relations from Drizzle schema
- ✅ Displays them as multi-select fields
- ✅ Shows them in both modal and page edit forms
- ✅ Handles sync operations
- ✅ Updates when you add new relations
Naming Convention Support
The M2M auto-detection system supports both camelCase and snake_case naming conventions. Your junction tables will be auto-detected regardless of naming style.
Supported Patterns
// ✅ snake_case (works automatically)
export const articleCategories = sqliteTable('article_categories', {
articleId: integer('article_id')
.references(() => articles.id, { onDelete: 'cascade' }),
categoryId: integer('category_id')
.references(() => categories.id, { onDelete: 'cascade' }),
}, (table) => ({
pk: primaryKey({ columns: [table.articleId, table.categoryId] })
}))
// ✅ camelCase (works automatically)
export const articleCategories = sqliteTable('articleCategories', {
articleId: integer('articleId')
.references(() => articles.id, { onDelete: 'cascade' }),
categoryId: integer('categoryId')
.references(() => categories.id, { onDelete: 'cascade' }),
}, (table) => ({
pk: primaryKey({ columns: [table.articleId, table.categoryId] })
}))
// ✅ Plural forms (works automatically)
export const articleCategories = sqliteTable('articles_categories', { ... })
// ✅ Mixed conventions (works automatically)
export const articleCategories = sqliteTable('article_categories', {
articleId: integer('articleId'), // camelCase column
categoryId: integer('category_id'), // snake_case column
})
Junction Table Hiding
Auto-detected junction tables are automatically:
- ✅ Hidden from admin sidebar
- ✅ Excluded from resource list
- ✅ Shown as M2M fields in edit forms
No configuration needed!
Manual Override
If you need to control detection for a specific table:
// Force junction table hidden
autoAdmin: {
resources: {
customJunction: {
type: 'junction' // Force hide from sidebar
}
}
}
// Force regular resource visible (despite junction pattern)
autoAdmin: {
resources: {
orderItems: {
type: 'resource', // Show in sidebar
displayName: 'Order Items',
}
}
}
For more details on auto-detection, see the API M2M Guide.
Configuration
Labels, Help Text, and Display Field
Configure how M2M relations appear in the admin UI:
autoApi: {
m2m: {
relations: {
articles: {
categories: {
// ... junction config
label: 'Article Categories', // Field label in form
help: 'Select categories to organize this article', // Help text below field
displayField: 'name', // What shows in dropdown options
}
}
}
}
}
If not provided:
label- Defaults to capitalized relation name ("Categories")help- Not showndisplayField- Tries common fields automatically:'name','title','label','email'
Display Field Examples:
// For categories (has 'name' field)
categories: {
displayField: 'name' // Shows: "Technology", "Business", etc.
}
// For users (has 'email' and 'name')
users: {
displayField: 'email' // Shows: "user@example.com"
}
// For articles (has 'title')
relatedArticles: {
displayField: 'title' // Shows: "How to Build..."
}
// Custom format (if needed)
products: {
displayField: 'sku' // Shows: "PROD-12345"
}
Display Modes
Control how edit/view forms open:
autoAdmin: {
ui: {
editMode: 'page', // 'modal' | 'page'
viewMode: 'modal' // 'modal' | 'page'
}
}
Modal Mode (default):
- Quick edits
- Compact view
- Good for simple forms
Page Mode (recommended for M2M):
- Full-page layout
- Better for complex forms with M2M relations
- More space for multiple relation fields
How It Works
Automatic Detection
When you open an edit form, the admin automatically:
- Fetches M2M config from the API module
- Loads current relations for the record
- Displays multi-select for each relation
- Syncs changes when you save
Manual Override
You can manually configure M2M fields in admin config (not recommended):
autoAdmin: {
resources: {
articles: {
formFields: {
edit: [
// Regular fields
{ name: 'title', widget: 'TextInput' },
// Manual M2M field (not recommended - use API config instead)
{
name: 'categories',
widget: 'MultiRelationSelect',
label: 'Categories',
options: {
resource: 'categories',
displayField: 'name',
junctionTable: 'articleCategories',
junctionLeftKey: 'articleId',
junctionRightKey: 'categoryId',
}
}
]
}
}
}
}
⚠️ Not Recommended: Manual admin config is verbose and duplicates API config. Use API module config instead!
UI Behavior
Edit Form
M2M relations appear as cards below the main form:
┌─────────────────────────────┐
│ Title: [..................] │
│ Content: [................] │
│ [Save] [Cancel] │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Categories │
│ Select categories for... │
│ │
│ [Multi-select dropdown] │
│ [Save Categories] [Reset] │
└─────────────────────────────┘
┌─────────────────────────────┐
│ Tags │
│ │
│ [Multi-select dropdown] │
│ [Save Tags] [Reset] │
└─────────────────────────────┘
View Modal
M2M relations shown as read-only chips:
┌─────────────────────────────┐
│ Article Title │
│ by John Doe │
│ │
│ Categories │
│ [Technology] [Business] │
│ │
│ Tags │
│ [JavaScript] [Vue] [Nuxt] │
└─────────────────────────────┘
Permissions
M2M fields respect resource permissions:
// modules/blog/auth.ts
export const articlesAuth = {
async canUpdate(user) {
// Controls both form fields AND M2M relations
return user?.role === 'editor'
}
}
If user lacks update permission:
- M2M fields shown but disabled
- Save buttons are disabled
- Or hidden based on
unauthorizedButtonsconfig
Advanced
Hiding Relations
Hide specific relations from admin UI:
autoAdmin: {
resources: {
articles: {
hiddenFields: ['internalCategoryId'], // Won't show in any form
}
}
}
Custom Relation Widget
Create custom widget for special cases:
<!-- components/admin/CustomRelationWidget.vue -->
<template>
<div>
<!-- Your custom multi-select UI -->
</div>
</template>
<script setup>
const props = defineProps<{
modelValue: number[]
options: any[]
}>()
const emit = defineEmits<{
'update:modelValue': [value: number[]]
}>()
</script>
Register in admin config:
autoAdmin: {
resources: {
articles: {
formFields: {
edit: [
{
name: 'categories',
widget: 'CustomRelationWidget', // Your widget
options: {
// Custom options
}
}
]
}
}
}
}
Metadata Support
Junction tables with metadata columns:
// Schema
export const articleCategories = sqliteTable('article_categories', {
articleId: integer('article_id')...
categoryId: integer('category_id')...
sortOrder: integer('sort_order'), // Metadata
isPrimary: integer('is_primary', { mode: 'boolean' }),
})
// Config
autoApi: {
m2m: {
relations: {
articles: {
categories: {
junctionTable: 'articleCategories',
leftKey: 'articleId',
rightKey: 'categoryId',
metadataColumns: ['sortOrder', 'isPrimary'],
}
}
}
}
}
The admin UI will:
- Show basic multi-select (no metadata editing by default)
- Metadata preserved on save
- Custom widget needed for editing metadata
Best Practices
1. Configure in API Module
// ✅ GOOD: Single source of truth
autoApi: {
m2m: {
relations: {
articles: {
categories: {
junctionTable: 'articleCategories',
leftKey: 'articleId',
rightKey: 'categoryId',
label: 'Categories', // For admin
}
}
}
}
}
// ❌ BAD: Duplicating config in admin module
autoAdmin: {
resources: {
articles: {
formFields: {
edit: [
{ /* manual M2M config */ }
]
}
}
}
}
2. Use Descriptive Labels
label: 'Article Categories', // ✅ Clear
help: 'Organize this article by selecting relevant categories' // ✅ Helpful
label: 'cats', // ❌ Unclear
3. Use Page Mode for Complex Forms
autoAdmin: {
ui: {
editMode: 'page', // ✅ Better for M2M
}
}
4. Test Permissions
Verify M2M fields respect permissions:
- Disabled when no update permission
- Hidden/shown based on config
Troubleshooting
Relations Not Showing
Problem: M2M fields don't appear in edit form
Solutions:
- Check API module config is correct
- Verify junction table is registered
- Check browser console for errors
- Test M2M endpoint directly:
GET /api/articles/10/relations/categories
Can't Save Relations
Problem: Save button doesn't work or shows errors
Solutions:
- Check permissions (need
canUpdate) - Verify related records exist
- Check browser network tab for API errors
- Ensure junction table has proper foreign keys
Wrong Labels
Problem: Relation shows wrong label
Solution: Update label in API config:
autoApi: {
m2m: {
relations: {
articles: {
categories: {
// ...
label: 'Correct Label Here', // ← Update this
}
}
}
}
}
Examples
Basic Setup
// nuxt.config.ts
export default defineNuxtConfig({
autoApi: {
m2m: {
// Auto-detection enabled by default
// Only customize labels if needed
relations: {
articles: {
categories: {
label: 'Categories',
displayField: 'name',
},
tags: {
label: 'Tags',
help: 'Add relevant tags',
}
}
}
}
},
autoAdmin: {
ui: {
editMode: 'page', // Use page mode for M2M
}
}
})
Multiple Resources
autoApi: {
m2m: {
// Auto-detection finds all junction tables
// Only customize labels/help as needed
relations: {
articles: {
categories: {
label: 'Categories',
},
tags: {
label: 'Tags',
},
},
users: {
roles: {
label: 'User Roles',
help: 'Assign roles to control permissions',
}
},
products: {
categories: {
label: 'Product Categories',
}
}
}
}
}
See Also
- API M2M Configuration - How to configure M2M relations
- Form Fields - Customizing form fields
- Permissions - Controlling access
- UI Configuration - Modal vs page mode
Custom Pages
Add custom pages to your admin panel for features beyond standard CRUD operations.
Custom Actions
Add custom actions to resources beyond standard CRUD operations.