Skip to content

@resourcefulHasManyThrough

The @resourcefulHasManyThrough decorator enhances AdonisJS Lucid hasManyThrough relationships with access control and metadata functionality.

For complete API reference, see resourcefulHasManyThrough.

Overview

The @resourcefulHasManyThrough decorator combines the functionality of Lucid's @hasManyThrough 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
  • Complex Queries: Enhanced query building for indirect relationships

Basic Usage

Simple HasManyThrough Relationship

typescript
import { resourcefulHasManyThrough } from '@nhtio/lucid-resourceful'
import { HasManyThrough } from '@adonisjs/lucid/types/relations'

class Country extends compose(BaseModel, withResourceful({ name: 'Country' })) {
  @resourcefulHasManyThrough([
    () => Post,        // Related model
    () => User,        // Through model
  ])
  declare posts: HasManyThrough<typeof Post>
}

With Through Keys

typescript
@resourcefulHasManyThrough([
  () => Post,           // Related model
  () => User,           // Through model
], {
  localKey: 'id',              // Country.id
  foreignKey: 'country_id',    // User.country_id
  throughLocalKey: 'id',       // User.id
  throughForeignKey: 'user_id' // Post.user_id
})
declare posts: HasManyThrough<typeof Post>

With Access Control

typescript
@resourcefulHasManyThrough([
  () => Post,
  () => User,
], {
  readAccessControlFilters: [
    // Only show public posts
    (ctx, app, country) => true,
    // Admins can see all posts
    (ctx) => ctx.auth.user?.role === 'admin'
  ]
})
declare posts: HasManyThrough<typeof Post>

Configuration Options

Lucid HasManyThrough Options

All standard Lucid hasManyThrough relationship options are supported:

typescript
@resourcefulHasManyThrough([
  () => Post,
  () => User,
], {
  localKey: 'id',                    // Local key column
  foreignKey: 'country_id',          // Foreign key in through model
  throughLocalKey: 'id',             // Through model's local key
  throughForeignKey: 'user_id',      // Foreign key in related model
  serializeAs: 'countryPosts',       // Serialization name
  onQuery: (query) => {              // Query modifications
    query.where('is_published', true)
         .orderBy('created_at', 'desc')
  }
})
declare posts: HasManyThrough<typeof Post>

Resourceful Relationship Options

readAccessControlFilters

  • Type: ResourcefulAccessControlFilter[]
  • Default: [] (no restrictions)

Control who can access the relationship data:

typescript
@resourcefulHasManyThrough([
  () => Post,
  () => User,
], {
  readAccessControlFilters: [
    (ctx, app, country) => {
      // Public posts for everyone
      return true
    },
    // Premium content for subscribers
    (ctx, app, country) => {
      return ctx.auth.user?.subscriptionLevel === 'premium'
    }
  ]
})
declare posts: HasManyThrough<typeof Post>

description

  • Type: string
  • Optional: Yes

Description for OpenAPI documentation:

typescript
@resourcefulHasManyThrough([
  () => Post,
  () => User,
], {
  description: 'All posts from users in this country'
})
declare posts: HasManyThrough<typeof Post>

Common Patterns

Country → Users → Posts

typescript
class Country extends compose(BaseModel, withResourceful({ name: 'Country' })) {
  @resourcefulHasManyThrough([
    () => Post,
    () => User,
  ], {
    localKey: 'id',
    foreignKey: 'country_id',
    throughLocalKey: 'id', 
    throughForeignKey: 'user_id',
    description: 'Posts from users in this country',
    readAccessControlFilters: [
      // Show published posts to everyone
      () => true
    ],
    onQuery: (query) => {
      query.where('posts.is_published', true)
           .where('posts.deleted_at', null)
           .orderBy('posts.created_at', 'desc')
    }
  })
  declare posts: HasManyThrough<typeof Post>
}

Organization → Departments → Employees

typescript
class Organization extends compose(BaseModel, withResourceful({ name: 'Organization' })) {
  @resourcefulHasManyThrough([
    () => Employee,
    () => Department,
  ], {
    localKey: 'id',
    foreignKey: 'organization_id',
    throughLocalKey: 'id',
    throughForeignKey: 'department_id',
    description: 'All employees across all departments',
    readAccessControlFilters: [
      // HR and managers can see all employees
      (ctx) => ['hr', 'manager'].includes(ctx.auth.user?.role),
      // Employees can see colleagues in same org
      (ctx, app, org) => ctx.auth.user?.organizationId === org.id
    ],
    onQuery: (query) => {
      query.where('employees.is_active', true)
           .orderBy('departments.name', 'asc')
           .orderBy('employees.name', 'asc')
    }
  })
  declare employees: HasManyThrough<typeof Employee>
}

Project → Teams → Members

typescript
class Project extends compose(BaseModel, withResourceful({ name: 'Project' })) {
  @resourcefulHasManyThrough([
    () => User,
    () => Team,
  ], {
    localKey: 'id',
    foreignKey: 'project_id',
    throughLocalKey: 'id',
    throughForeignKey: 'team_id',
    serializeAs: 'allMembers',
    description: 'All team members working on this project',
    readAccessControlFilters: [
      // Project managers can see all members
      (ctx, app, project) => project.managerId === ctx.auth.user?.id,
      // Team leads can see all members
      (ctx, app, project) => {
        return project.teams?.some(team => 
          team.leaderId === ctx.auth.user?.id
        )
      },
      // Members can see other members
      (ctx, app, project) => {
        return project.allMembers?.some(member => 
          member.id === ctx.auth.user?.id
        )
      }
    ],
    onQuery: (query) => {
      query.where('users.is_active', true)
           .where('teams.is_active', true)
           .orderBy('teams.name', 'asc')
           .orderBy('users.name', 'asc')
    }
  })
  declare allMembers: HasManyThrough<typeof User>
}

Customer → Orders → Products

typescript
class Customer extends compose(BaseModel, withResourceful({ name: 'Customer' })) {
  @resourcefulHasManyThrough([
    () => Product,
    () => Order,
  ], {
    localKey: 'id',
    foreignKey: 'customer_id',
    throughLocalKey: 'id',
    throughForeignKey: 'order_id',
    description: 'Products purchased by this customer',
    readAccessControlFilters: [
      // Customers can see their own purchases
      (ctx, app, customer) => ctx.auth.user?.id === customer.userId,
      // Sales team can see customer purchases
      (ctx) => ctx.auth.user?.role === 'sales',
      // Support can see purchases for tickets
      (ctx, app, customer) => {
        return ctx.auth.user?.role === 'support' && 
               ctx.request.header('support-ticket-id')
      }
    ],
    onQuery: (query) => {
      query.where('orders.status', 'completed')
           .where('products.is_active', true)
           .orderBy('orders.created_at', 'desc')
    }
  })
  declare purchasedProducts: HasManyThrough<typeof Product>
}

Course → Students → Assignments

typescript
class Course extends compose(BaseModel, withResourceful({ name: 'Course' })) {
  @resourcefulHasManyThrough([
    () => Assignment,
    () => Student,
  ], {
    localKey: 'id',
    foreignKey: 'course_id',
    throughLocalKey: 'id',
    throughForeignKey: 'student_id',
    description: 'All assignments submitted by students in this course',
    readAccessControlFilters: [
      // Instructors can see all assignments
      (ctx, app, course) => course.instructorId === ctx.auth.user?.id,
      // TAs can see assignments
      (ctx, app, course) => {
        return course.teachingAssistants?.some(ta => 
          ta.id === ctx.auth.user?.id
        )
      },
      // Students can only see their own assignments
      (ctx, app, course) => {
        return course.students?.some(student => 
          student.userId === ctx.auth.user?.id
        )
      }
    ],
    onQuery: (query, { ctx }) => {
      // If student, filter to their assignments only
      if (ctx.auth.user?.role === 'student') {
        query.where('students.user_id', ctx.auth.user.id)
      }
      
      query.where('assignments.is_active', true)
           .orderBy('assignments.due_date', 'desc')
    }
  })
  declare allAssignments: HasManyThrough<typeof Assignment>
}

Advanced Examples

Multi-tenant Access Control

typescript
class Company extends compose(BaseModel, withResourceful({ name: 'Company' })) {
  @resourcefulHasManyThrough([
    () => Invoice,
    () => Customer,
  ], {
    localKey: 'id',
    foreignKey: 'company_id',
    throughLocalKey: 'id',
    throughForeignKey: 'customer_id',
    description: 'All invoices for customers of this company',
    readAccessControlFilters: [
      // Ensure tenant isolation
      (ctx, app, company) => {
        return ctx.auth.user?.companyId === company.id
      },
      // Accountants can see all company invoices
      (ctx, app, company) => {
        return ctx.auth.user?.role === 'accountant' && 
               ctx.auth.user?.companyId === company.id
      },
      // Managers can see invoices for their customers
      (ctx, app, company) => {
        if (ctx.auth.user?.role !== 'manager') return false
        return company.customers?.some(customer => 
          customer.managedBy === ctx.auth.user?.id
        )
      }
    ],
    onQuery: (query, { ctx }) => {
      // Enforce tenant isolation at query level
      query.where('companies.id', ctx.auth.user?.companyId)
           .where('invoices.deleted_at', null)
           .orderBy('invoices.created_at', 'desc')
    }
  })
  declare allInvoices: HasManyThrough<typeof Invoice>
}

Time-based Filtering

typescript
class Region extends compose(BaseModel, withResourceful({ name: 'Region' })) {
  @resourcefulHasManyThrough([
    () => Sale,
    () => Store,
  ], {
    localKey: 'id',
    foreignKey: 'region_id',
    throughLocalKey: 'id',
    throughForeignKey: 'store_id',
    description: 'Sales from all stores in this region',
    readAccessControlFilters: [
      // Regional managers can see all sales
      (ctx, app, region) => {
        return ctx.auth.user?.role === 'regional_manager' && 
               ctx.auth.user?.regionId === region.id
      },
      // Store managers can see their store's sales
      (ctx, app, region) => {
        return ctx.auth.user?.role === 'store_manager' && 
               region.stores?.some(store => 
                 store.managerId === ctx.auth.user?.id
               )
      },
      // Analysts can see aggregated data
      (ctx) => ctx.auth.user?.role === 'analyst'
    ],
    onQuery: (query, { ctx }) => {
      // Filter by date range if provided
      const { startDate, endDate } = ctx.request.qs()
      
      if (startDate) {
        query.where('sales.created_at', '>=', startDate)
      }
      
      if (endDate) {
        query.where('sales.created_at', '<=', endDate)
      }
      
      // Only completed sales
      query.where('sales.status', 'completed')
           .orderBy('sales.created_at', 'desc')
    }
  })
  declare sales: HasManyThrough<typeof Sale>
}

Hierarchical Relationships

typescript
class Division extends compose(BaseModel, withResourceful({ name: 'Division' })) {
  @resourcefulHasManyThrough([
    () => Task,
    () => Employee,
  ], {
    localKey: 'id',
    foreignKey: 'division_id',
    throughLocalKey: 'id',
    throughForeignKey: 'assigned_to',
    description: 'All tasks assigned to employees in this division',
    readAccessControlFilters: [
      // Division heads can see all tasks
      (ctx, app, division) => division.headId === ctx.auth.user?.id,
      // Managers can see tasks for their reports
      (ctx, app, division) => {
        return division.employees?.some(emp => 
          emp.managerId === ctx.auth.user?.id
        )
      },
      // Employees can see their own tasks
      (ctx, app, division) => {
        return division.employees?.some(emp => 
          emp.id === ctx.auth.user?.id
        )
      }
    ],
    onQuery: (query, { ctx }) => {
      const userRole = ctx.auth.user?.role
      const userId = ctx.auth.user?.id
      
      // Filter based on user permissions
      if (userRole === 'employee') {
        query.where('tasks.assigned_to', userId)
      } else if (userRole === 'manager') {
        query.whereExists(builder => {
          builder.select('*')
                .from('employees')
                .whereRaw('employees.id = tasks.assigned_to')
                .where('employees.manager_id', userId)
        })
      }
      
      query.where('tasks.deleted_at', null)
           .orderBy('tasks.priority', 'desc')
           .orderBy('tasks.due_date', 'asc')
    }
  })
  declare allTasks: HasManyThrough<typeof Task>
}

Complete Example

typescript
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { HasManyThrough } from '@adonisjs/lucid/types/relations'
import { 
  withResourceful, 
  resourcefulColumn, 
  resourcefulHasManyThrough 
} from '@nhtio/lucid-resourceful'
import { 
  ResourcefulUnsignedIntegerType,
  ResourcefulStringType
} from '@nhtio/lucid-resourceful/definitions'

export default class Organization extends compose(BaseModel, withResourceful({ 
  name: 'Organization' 
})) {
  @resourcefulColumn({ 
    isPrimary: true,
    type: ResourcefulUnsignedIntegerType({ readOnly: true })
  })
  declare id: number

  @resourcefulColumn.string({
    type: ResourcefulStringType({ minLength: 1, maxLength: 100 })
  })
  declare name: string

  // All employees across departments
  @resourcefulHasManyThrough([
    () => Employee,
    () => Department,
  ], {
    localKey: 'id',
    foreignKey: 'organization_id',
    throughLocalKey: 'id',
    throughForeignKey: 'department_id',
    description: 'All employees in this organization',
    readAccessControlFilters: [
      // HR can see all employees
      (ctx) => ctx.auth.user?.role === 'hr',
      // Managers can see employees in their org
      (ctx, app, org) => {
        return ctx.auth.user?.role === 'manager' && 
               ctx.auth.user?.organizationId === org.id
      },
      // Employees can see colleagues
      (ctx, app, org) => ctx.auth.user?.organizationId === org.id
    ],
    onQuery: (query, { ctx }) => {
      // Filter by active employees
      query.where('employees.is_active', true)
           .where('departments.is_active', true)
      
      // Department heads see only their department
      if (ctx.auth.user?.role === 'department_head') {
        query.where('departments.head_id', ctx.auth.user.id)
      }
      
      query.orderBy('departments.name', 'asc')
           .orderBy('employees.name', 'asc')
    }
  })
  declare allEmployees: HasManyThrough<typeof Employee>

  // All projects through teams
  @resourcefulHasManyThrough([
    () => Project,
    () => Team,
  ], {
    localKey: 'id',
    foreignKey: 'organization_id',
    throughLocalKey: 'id',
    throughForeignKey: 'team_id',
    serializeAs: 'projects',
    description: 'All projects managed by teams in this organization',
    readAccessControlFilters: [
      // Project managers can see all projects
      (ctx) => ctx.auth.user?.role === 'project_manager',
      // Team members can see their projects
      (ctx, app, org) => {
        return org.allEmployees?.some(emp => 
          emp.id === ctx.auth.user?.id
        )
      }
    ],
    onQuery: (query, { ctx }) => {
      // Filter by project status
      const status = ctx.request.qs().status
      if (status) {
        query.where('projects.status', status)
      }
      
      query.where('projects.is_active', true)
           .where('teams.is_active', true)
           .orderBy('projects.priority', 'desc')
           .orderBy('projects.created_at', 'desc')
    }
  })
  declare projects: HasManyThrough<typeof Project>

  // All invoices through customers
  @resourcefulHasManyThrough([
    () => Invoice,
    () => Customer,
  ], {
    localKey: 'id',
    foreignKey: 'organization_id',
    throughLocalKey: 'id',
    throughForeignKey: 'customer_id',
    description: 'All invoices for customers of this organization',
    readAccessControlFilters: [
      // Finance team can see all invoices
      (ctx) => ctx.auth.user?.role === 'finance',
      // Account managers can see their customer invoices
      (ctx, app, org) => {
        return ctx.auth.user?.role === 'account_manager' &&
               org.customers?.some(customer => 
                 customer.managedBy === ctx.auth.user?.id
               )
      }
    ],
    onQuery: (query, { ctx }) => {
      // Default to recent invoices
      query.where('invoices.created_at', '>=', 
                  new Date(Date.now() - 90 * 24 * 60 * 60 * 1000))
           .where('invoices.deleted_at', null)
           .orderBy('invoices.created_at', 'desc')
      
      // Filter by payment status if specified
      const paymentStatus = ctx.request.qs().payment_status
      if (paymentStatus) {
        query.where('invoices.payment_status', paymentStatus)
      }
    }
  })
  declare recentInvoices: HasManyThrough<typeof Invoice>

  @resourcefulColumn.dateTime({ autoCreate: true })
  declare createdAt: DateTime

  @resourcefulColumn.dateTime({ autoCreate: true, autoUpdate: true })
  declare updatedAt: DateTime
}

Query Performance Tips

Optimize Through Relationships

typescript
// Good: Index the through relationship properly
@resourcefulHasManyThrough([
  () => Post,
  () => User,
], {
  onQuery: (query) => {
    // Use specific select to avoid N+1 queries
    query.select([
      'posts.*',
      'users.name as author_name',
      'users.avatar as author_avatar'
    ])
    .where('posts.is_published', true)
  }
})

Limit Result Sets

typescript
@resourcefulHasManyThrough([
  () => Product,
  () => Order,
], {
  onQuery: (query, { ctx }) => {
    // Implement pagination
    const page = parseInt(ctx.request.qs().page) || 1
    const limit = parseInt(ctx.request.qs().limit) || 10
    
    query.offset((page - 1) * limit)
         .limit(limit)
         .orderBy('orders.created_at', 'desc')
  }
})

Best Practices

  1. Index foreign keys: Ensure proper database indexes on all relationship keys
  2. Use specific selects: Avoid selecting unnecessary columns in through queries
  3. Implement pagination: Limit result sets for performance
  4. Filter early: Apply WHERE conditions before ordering and limiting
  5. Use descriptive names: Name relationships clearly to indicate the through relationship
  6. Consider performance: HasManyThrough can be expensive - monitor query performance
  7. Add proper access control: Secure indirect relationships appropriately
  8. Document complex relationships: Explain the relationship path in descriptions
  9. Use onQuery effectively: Filter and optimize at the database level
  10. Test with realistic data: Ensure performance with production-like dataset sizes