@resourcefulHasMany
The @resourcefulHasMany decorator enhances AdonisJS Lucid hasMany relationships with access control and metadata functionality.
For complete API reference, see resourcefulHasMany.
Overview
The @resourcefulHasMany decorator combines the functionality of Lucid's @hasMany 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
Basic Usage
Simple HasMany Relationship
typescript
import { resourcefulHasMany } from '@nhtio/lucid-resourceful'
import { HasMany } from '@adonisjs/lucid/types/relations'
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id'
})
declare posts: HasMany<typeof Post>
}With Access Control
typescript
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
readAccessControlFilters: [
// Only authenticated users can see posts
(ctx) => !!ctx.auth.user,
// Users can see their own posts, admins can see all
(ctx, app, user) => {
return ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
}
]
})
declare posts: HasMany<typeof Post>Configuration Options
Lucid HasMany Options
All standard Lucid hasMany relationship options are supported:
typescript
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id', // Foreign key on related model
localKey: 'id', // Local key column (defaults to primary key)
serializeAs: 'articles', // Serialization name
onQuery: (query) => { // Query modifications
query.where('is_published', true)
.orderBy('created_at', 'desc')
.select('id', 'title', 'excerpt', 'created_at')
}
})
declare posts: HasMany<typeof Post>Resourceful Relationship Options
readAccessControlFilters
- Type:
ResourcefulAccessControlFilter[] - Default:
[](no restrictions)
Control who can access the relationship data:
typescript
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
readAccessControlFilters: [
(ctx, app, user) => {
// Allow if viewing own posts or user is admin
return ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
}
]
})
declare posts: HasMany<typeof Post>description
- Type:
string - Optional: Yes
Description for OpenAPI documentation:
typescript
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
description: 'All posts created by this user'
})
declare posts: HasMany<typeof Post>Common Patterns
Published Posts Only
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
serializeAs: 'publishedPosts',
description: 'Published posts by this user',
readAccessControlFilters: [
(ctx) => !!ctx.auth.user
],
onQuery: (query) => {
query.where('is_published', true)
.where('published_at', '<=', new Date())
.orderBy('published_at', 'desc')
}
})
declare publishedPosts: HasMany<typeof Post>
}Comments with Moderation
typescript
class Post extends compose(BaseModel, withResourceful({ name: 'Post' })) {
@resourcefulHasMany(() => Comment, {
foreignKey: 'post_id',
description: 'Comments on this post',
readAccessControlFilters: [
// Show approved comments to everyone
(ctx, app, post) => !!ctx.auth.user,
// Show all comments to post author and admins
(ctx, app, post) => {
return ctx.auth.user?.id === post.userId ||
ctx.auth.user?.role === 'admin'
}
],
onQuery: (query, { ctx }) => {
// Filter based on user permissions
if (ctx.auth.user?.role !== 'admin' &&
ctx.auth.user?.id !== ctx.params.id) {
query.where('is_approved', true)
}
query.orderBy('created_at', 'asc')
}
})
declare comments: HasMany<typeof Comment>
}Orders with Status Filtering
typescript
class Customer extends compose(BaseModel, withResourceful({ name: 'Customer' })) {
@resourcefulHasMany(() => Order, {
foreignKey: 'customer_id',
description: 'Customer orders',
readAccessControlFilters: [
// Customers can see their own orders
(ctx, app, customer) => ctx.auth.user?.customerId === customer.id,
// Staff can see orders for customers in their territory
(ctx, app, customer) => {
return ctx.auth.user?.role === 'staff' &&
ctx.auth.user?.territory === customer.territory
},
// Admins can see all orders
(ctx) => ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.orderBy('created_at', 'desc')
}
})
declare orders: HasMany<typeof Order>
// Separate relationship for active orders only
@resourcefulHasMany(() => Order, {
foreignKey: 'customer_id',
serializeAs: 'activeOrders',
description: 'Active customer orders',
readAccessControlFilters: [
(ctx, app, customer) => ctx.auth.user?.customerId === customer.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.whereIn('status', ['pending', 'processing', 'shipped'])
.orderBy('created_at', 'desc')
}
})
declare activeOrders: HasMany<typeof Order>
}Hierarchical Relationships
typescript
class Category extends compose(BaseModel, withResourceful({ name: 'Category' })) {
@resourcefulHasMany(() => Category, {
foreignKey: 'parent_id',
serializeAs: 'subcategories',
description: 'Child categories',
readAccessControlFilters: [
// Only show active subcategories
(ctx, app, category) => category.isActive
],
onQuery: (query) => {
query.where('is_active', true)
.orderBy('sort_order', 'asc')
}
})
declare subcategories: HasMany<typeof Category>
@resourcefulHasMany(() => Product, {
foreignKey: 'category_id',
description: 'Products in this category',
onQuery: (query) => {
query.where('is_active', true)
.orderBy('name', 'asc')
}
})
declare products: HasMany<typeof Product>
}Multi-tenant Relationships
typescript
class Organization extends compose(BaseModel, withResourceful({ name: 'Organization' })) {
@resourcefulHasMany(() => User, {
foreignKey: 'organization_id',
description: 'Users in this organization',
readAccessControlFilters: [
// Users can see members of their own organization
(ctx, app, org) => ctx.auth.user?.organizationId === org.id,
// Global admins can see all
(ctx) => ctx.auth.user?.role === 'global_admin'
],
onQuery: (query) => {
query.where('is_active', true)
.orderBy('name', 'asc')
}
})
declare users: HasMany<typeof User>
@resourcefulHasMany(() => Project, {
foreignKey: 'organization_id',
description: 'Projects owned by this organization',
readAccessControlFilters: [
(ctx, app, org) => {
// Organization members can see projects
if (ctx.auth.user?.organizationId === org.id) return true
// External users can see public projects only
return false // Will be handled by onQuery
}
],
onQuery: (query, { ctx }) => {
if (ctx.auth.user?.organizationId !== ctx.params.id) {
query.where('is_public', true)
}
query.orderBy('created_at', 'desc')
}
})
declare projects: HasMany<typeof Project>
}Advanced Examples
Time-based Filtering
typescript
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulHasMany(() => Activity, {
foreignKey: 'user_id',
serializeAs: 'recentActivities',
description: 'User activities from the last 30 days',
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
query.where('created_at', '>=', thirtyDaysAgo)
.orderBy('created_at', 'desc')
.limit(100)
}
})
declare recentActivities: HasMany<typeof Activity>
}Conditional Relationship Loading
typescript
class Event extends compose(BaseModel, withResourceful({ name: 'Event' })) {
@resourcefulHasMany(() => Registration, {
foreignKey: 'event_id',
description: 'Event registrations',
readAccessControlFilters: [
// Event organizers can see all registrations
(ctx, app, event) => ctx.auth.user?.id === event.organizerId,
// Users can see their own registration
(ctx, app, event) => {
return event.registrations?.some(reg =>
reg.userId === ctx.auth.user?.id
)
},
// Public events show approved registrations only
(ctx, app, event) => event.isPublic
],
onQuery: (query, { ctx, instance }) => {
// Filter based on user role and relationship to event
if (ctx.auth.user?.id !== instance.organizerId &&
!instance.isPublic) {
query.where('user_id', ctx.auth.user?.id)
} else if (!ctx.auth.user?.role === 'admin') {
query.where('status', 'approved')
}
query.orderBy('created_at', 'asc')
}
})
declare registrations: HasMany<typeof Registration>
}Complete Example
typescript
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { HasMany } from '@adonisjs/lucid/types/relations'
import {
withResourceful,
resourcefulColumn,
resourcefulHasMany
} 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.string({
type: ResourcefulStringType()
})
declare role: string
@resourcefulColumn.boolean({
type: ResourcefulBooleanType()
})
declare isActive: boolean
// All posts (with access control)
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
description: 'All posts by this user',
readAccessControlFilters: [
// Users can see their own posts
(ctx, app, user) => ctx.auth.user?.id === user.id,
// Admins can see all posts
(ctx) => ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.orderBy('created_at', 'desc')
}
})
declare posts: HasMany<typeof Post>
// Published posts only (public access)
@resourcefulHasMany(() => Post, {
foreignKey: 'user_id',
serializeAs: 'publishedPosts',
description: 'Published posts by this user',
readAccessControlFilters: [
(ctx) => !!ctx.auth.user
],
onQuery: (query) => {
query.where('is_published', true)
.where('published_at', '<=', new Date())
.orderBy('published_at', 'desc')
.select('id', 'title', 'excerpt', 'published_at')
}
})
declare publishedPosts: HasMany<typeof Post>
// Comments made by user
@resourcefulHasMany(() => Comment, {
foreignKey: 'user_id',
serializeAs: 'comments',
description: 'Comments made by this user',
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user.id ||
ctx.auth.user?.role === 'admin'
],
onQuery: (query) => {
query.where('is_approved', true)
.orderBy('created_at', 'desc')
.limit(50)
}
})
declare comments: HasMany<typeof Comment>
@resourcefulColumn.dateTime({ autoCreate: true })
declare createdAt: DateTime
@resourcefulColumn.dateTime({ autoCreate: true, autoUpdate: true })
declare updatedAt: DateTime
}Best Practices
- Use descriptive relationship names: Choose clear names for relationship properties
- Implement appropriate access control: Secure relationship data based on user permissions
- Use onQuery for filtering: Filter related records at the database level
- Optimize queries: Select only necessary columns and limit results when appropriate
- Handle large datasets: Consider pagination for relationships with many records
- Use serializeAs for clarity: Rename relationships for better API field names
- Add descriptions: Document what each relationship represents
- Consider performance: Be mindful of N+1 query problems
- Implement tenant scoping: Add tenant checks for multi-tenant applications
- Use multiple relationships: Create specific relationships for different use cases (e.g., active vs all records)