@resourcefulManyToMany
The @resourcefulManyToMany decorator enhances AdonisJS Lucid manyToMany relationships with access control and metadata functionality.
For complete API reference, see resourcefulManyToMany.
Overview
The @resourcefulManyToMany decorator combines the functionality of Lucid's @manyToMany decorator with resourceful features:
- Access Control: Relationship-level read permissions
- OpenAPI Integration: Schema generation for API documentation
- Metadata Storage: Rich metadata for runtime introspection
- Type Safety: Full TypeScript support for related models
- Pivot Table Support: Enhanced pivot table functionality
Basic Usage
Simple ManyToMany Relationship
typescript
import { resourcefulManyToMany } from '@nhtio/lucid-resourceful'
import { ManyToMany } from '@adonisjs/lucid/types/relations'
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles'
})
declare roles: ManyToMany<typeof Role>
}With Access Control
typescript
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
readAccessControlFilters: [
// Only authenticated users can see roles
(ctx) => !!ctx.auth.user,
// Users can see their own roles, admins can see all
(ctx, app, user) => {
return ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
}
]
})
declare roles: ManyToMany<typeof Role>Configuration Options
Lucid ManyToMany Options
All standard Lucid manyToMany relationship options are supported:
typescript
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles', // Pivot table name
localKey: 'id', // Local key column
pivotForeignKey: 'user_id', // Local foreign key in pivot
relatedKey: 'id', // Related model key
pivotRelatedForeignKey: 'role_id', // Related foreign key in pivot
pivotColumns: ['assigned_at', 'assigned_by'], // Additional pivot columns
serializeAs: 'userRoles', // Serialization name
onQuery: (query) => { // Query modifications
query.where('is_active', true)
.orderBy('name', 'asc')
}
})
declare roles: ManyToMany<typeof Role>Resourceful Relationship Options
readAccessControlFilters
- Type:
ResourcefulAccessControlFilter[] - Default:
[](no restrictions)
Control who can access the relationship data:
typescript
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
readAccessControlFilters: [
(ctx, app, user) => {
// Allow if viewing own roles or user is admin
return ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
}
]
})
declare roles: ManyToMany<typeof Role>description
- Type:
string - Optional: Yes
Description for OpenAPI documentation:
typescript
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
description: 'Roles assigned to this user'
})
declare roles: ManyToMany<typeof Role>Common Patterns
User Roles with Timestamps
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
pivotColumns: ['assigned_at', 'assigned_by', 'expires_at'],
description: 'Roles assigned to this user',
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
// Only include active, non-expired roles
query.whereNull('expires_at')
.orWhere('expires_at', '>', new Date())
}
})
declare roles: ManyToMany<typeof Role>
}Product Categories
typescript
class Product extends compose(BaseModel, withResourceful({ name: 'Product' })) {
@resourcefulManyToMany(() => Category, {
pivotTable: 'product_categories',
pivotColumns: ['sort_order', 'is_primary'],
description: 'Categories this product belongs to',
readAccessControlFilters: [
// Anyone can see product categories
() => true
],
onQuery: (query) => {
query.where('is_active', true)
.orderBy('pivot_sort_order', 'asc')
}
})
declare categories: ManyToMany<typeof Category>
}User Permissions
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Permission, {
pivotTable: 'user_permissions',
pivotColumns: ['granted_at', 'granted_by', 'scope'],
description: 'Direct permissions granted to this user',
readAccessControlFilters: [
// Only admins and the user themselves can see permissions
(ctx, app, user) => ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.where('is_active', true)
}
})
declare permissions: ManyToMany<typeof Permission>
}Team Memberships
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Team, {
pivotTable: 'team_members',
pivotColumns: ['joined_at', 'role', 'is_active'],
description: 'Teams this user belongs to',
readAccessControlFilters: [
// Users can see their own team memberships
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Team members can see other members
(ctx, app, user) => {
return user.teams?.some(team =>
team.pivot.isActive &&
ctx.auth.user?.teams?.some(userTeam =>
userTeam.id === team.id && userTeam.pivot.isActive
)
)
},
// Admins can see all
(ctx) => ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.where('pivot_is_active', true)
.orderBy('pivot_joined_at', 'desc')
}
})
declare teams: ManyToMany<typeof Team>
}Skills and Endorsements
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Skill, {
pivotTable: 'user_skills',
pivotColumns: ['level', 'years_experience', 'endorsed_count'],
description: 'Skills possessed by this user',
readAccessControlFilters: [
// Public profiles show skills
(ctx, app, user) => user.isPublic,
// Users can see their own skills
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Connections can see skills
(ctx, app, user) => {
return user.connections?.some(conn =>
conn.id === ctx.auth.user?.id && conn.pivot.status === 'accepted'
)
}
],
onQuery: (query) => {
query.orderBy('pivot_level', 'desc')
.orderBy('pivot_endorsed_count', 'desc')
}
})
declare skills: ManyToMany<typeof Skill>
}Advanced Examples
Multi-tenant Team Access
typescript
class Project extends compose(BaseModel, withResourceful({ name: 'Project' })) {
@resourcefulManyToMany(() => User, {
pivotTable: 'project_members',
pivotColumns: ['role', 'joined_at', 'permissions'],
serializeAs: 'members',
description: 'Users who have access to this project',
readAccessControlFilters: [
// Project members can see other members
(ctx, app, project) => {
return project.members?.some(member =>
member.id === ctx.auth.user?.id
)
},
// Organization admins can see members
(ctx, app, project) => {
return ctx.auth.user?.role === 'admin' &&
ctx.auth.user?.organizationId === project.organizationId
}
],
onQuery: (query, { ctx }) => {
// Ensure tenant isolation
query.where('organization_id', ctx.auth.user?.organizationId)
.orderBy('pivot_joined_at', 'asc')
}
})
declare members: ManyToMany<typeof User>
}Time-based Access Control
typescript
class Course extends compose(BaseModel, withResourceful({ name: 'Course' })) {
@resourcefulManyToMany(() => User, {
pivotTable: 'course_enrollments',
pivotColumns: ['enrolled_at', 'completed_at', 'access_expires_at', 'status'],
serializeAs: 'students',
description: 'Students enrolled in this course',
readAccessControlFilters: [
// Instructors can see all students
(ctx, app, course) => course.instructors?.some(inst =>
inst.id === ctx.auth.user?.id
),
// Students can see other students if course allows it
(ctx, app, course) => {
if (!course.allowStudentList) return false
return course.students?.some(student =>
student.id === ctx.auth.user?.id &&
student.pivot.status === 'active' &&
(!student.pivot.accessExpiresAt ||
student.pivot.accessExpiresAt > new Date())
)
}
],
onQuery: (query) => {
query.where('pivot_status', 'active')
.where(builder => {
builder.whereNull('pivot_access_expires_at')
.orWhere('pivot_access_expires_at', '>', new Date())
})
.orderBy('pivot_enrolled_at', 'asc')
}
})
declare students: ManyToMany<typeof User>
}Hierarchical Permissions
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulManyToMany(() => Resource, {
pivotTable: 'user_resource_permissions',
pivotColumns: ['permission_type', 'granted_at', 'granted_by', 'inherited_from'],
description: 'Resources this user has access to',
readAccessControlFilters: [
// Users can see their own permissions
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Managers can see permissions for their reports
(ctx, app, user) => user.managerId === ctx.auth.user?.id,
// Admins can see all
(ctx) => ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.whereIn('pivot_permission_type', ['read', 'write', 'admin'])
.orderBy('pivot_permission_type', 'desc')
.orderBy('pivot_granted_at', 'desc')
}
})
declare accessibleResources: ManyToMany<typeof Resource>
}Complete Example
typescript
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { ManyToMany } from '@adonisjs/lucid/types/relations'
import {
withResourceful,
resourcefulColumn,
resourcefulManyToMany
} from '@nhtio/lucid-resourceful'
import {
ResourcefulUnsignedIntegerType,
ResourcefulStringType,
ResourcefulBooleanType,
ResourcefulDateTimeType
} from '@nhtio/lucid-resourceful/definitions'
export default class User extends compose(BaseModel, withResourceful({
name: 'User'
})) {
@resourcefulColumn({
isPrimary: true,
type: ResourcefulUnsignedIntegerType({ readOnly: true })
})
declare id: number
@resourcefulColumn.string({
type: ResourcefulStringType({ minLength: 1, maxLength: 100 })
})
declare name: string
@resourcefulColumn.boolean({
type: ResourcefulBooleanType()
})
declare isPublic: boolean
// User roles with assignment tracking
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
pivotColumns: ['assigned_at', 'assigned_by', 'expires_at'],
description: 'Roles assigned to this user',
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.where('is_active', true)
.where(builder => {
builder.whereNull('pivot_expires_at')
.orWhere('pivot_expires_at', '>', new Date())
})
.orderBy('name', 'asc')
}
})
declare roles: ManyToMany<typeof Role>
// Skills with proficiency levels
@resourcefulManyToMany(() => Skill, {
pivotTable: 'user_skills',
pivotColumns: ['level', 'years_experience', 'endorsed_count', 'verified'],
description: 'Skills and expertise areas',
readAccessControlFilters: [
// Public profiles show verified skills
(ctx, app, user) => user.isPublic,
// Users can see their own skills
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Connected users can see skills
(ctx, app, user) => {
return user.connections?.some(conn =>
conn.id === ctx.auth.user?.id &&
conn.pivot.status === 'accepted'
)
}
],
onQuery: (query, { ctx, instance }) => {
// Show different skill sets based on access level
if (ctx.auth.user?.id === instance.id) {
// User can see all their skills
query.orderBy('pivot_level', 'desc')
} else {
// Others see only verified or highly endorsed skills
query.where(builder => {
builder.where('pivot_verified', true)
.orWhere('pivot_endorsed_count', '>=', 5)
})
.orderBy('pivot_endorsed_count', 'desc')
}
}
})
declare skills: ManyToMany<typeof Skill>
// Team memberships
@resourcefulManyToMany(() => Team, {
pivotTable: 'team_members',
pivotColumns: ['role', 'joined_at', 'is_active'],
description: 'Teams this user belongs to',
readAccessControlFilters: [
// Users can see their own teams
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Team members can see each other
(ctx, app, user) => {
return user.teams?.some(team =>
team.pivot.isActive &&
ctx.auth.user?.teams?.some(userTeam =>
userTeam.id === team.id && userTeam.pivot.isActive
)
)
},
// Admins can see all teams
(ctx) => ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.where('pivot_is_active', true)
.where('is_active', true)
.orderBy('pivot_joined_at', 'desc')
}
})
declare teams: ManyToMany<typeof Team>
@resourcefulColumn.dateTime({ autoCreate: true })
declare createdAt: DateTime
@resourcefulColumn.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}Pivot Table Best Practices
Naming Conventions
typescript
// Good: Clear table and column names
@resourcefulManyToMany(() => Role, {
pivotTable: 'user_roles',
pivotForeignKey: 'user_id',
pivotRelatedForeignKey: 'role_id'
})
// Good: Descriptive pivot columns
@resourcefulManyToMany(() => Team, {
pivotTable: 'team_members',
pivotColumns: ['joined_at', 'role', 'is_active', 'invited_by']
})Common Pivot Patterns
typescript
// Audit trail pattern
pivotColumns: ['created_at', 'created_by', 'updated_at', 'updated_by']
// Status tracking pattern
pivotColumns: ['status', 'status_changed_at', 'notes']
// Hierarchical pattern
pivotColumns: ['parent_id', 'level', 'path']
// Approval workflow pattern
pivotColumns: ['status', 'requested_at', 'approved_at', 'approved_by']Best Practices
- Use descriptive pivot table names: Follow
model1_model2convention - Include audit columns: Track when and by whom relationships were created
- Implement appropriate access control: Secure relationship data based on permissions
- Use pivot columns effectively: Store relationship-specific metadata
- Handle inactive relationships: Use status columns to manage active/inactive states
- Optimize queries: Filter at the database level using onQuery
- Consider time-based access: Implement expiration dates for temporary access
- Use serializeAs for clarity: Rename relationships for better API field names
- Document relationships: Add clear descriptions for complex many-to-many relationships
- Handle large datasets: Consider pagination and limiting for relationships with many records