Skip to content

@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

  1. Use descriptive relationship names: Choose clear names for relationship properties
  2. Implement appropriate access control: Secure relationship data based on user permissions
  3. Use onQuery for filtering: Filter related records at the database level
  4. Optimize queries: Select only necessary columns and limit results when appropriate
  5. Handle large datasets: Consider pagination for relationships with many records
  6. Use serializeAs for clarity: Rename relationships for better API field names
  7. Add descriptions: Document what each relationship represents
  8. Consider performance: Be mindful of N+1 query problems
  9. Implement tenant scoping: Add tenant checks for multi-tenant applications
  10. Use multiple relationships: Create specific relationships for different use cases (e.g., active vs all records)