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