Skip to content

@resourcefulBelongsTo

The @resourcefulBelongsTo decorator enhances AdonisJS Lucid belongsTo relationships with access control and metadata functionality.

For complete API reference, see resourcefulBelongsTo.

Overview

The @resourcefulBelongsTo decorator combines the functionality of Lucid's @belongsTo 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 BelongsTo Relationship

typescript
import { resourcefulBelongsTo } from '@nhtio/lucid-resourceful'
import { BelongsTo } from '@adonisjs/lucid/types/relations'

class Post extends compose(BaseModel, withResourceful({ name: 'Post' })) {
  @resourcefulBelongsTo(() => User, {
    foreignKey: 'user_id'
  })
  declare user: BelongsTo<typeof User>
}

With Access Control

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'user_id',
  readAccessControlFilters: [
    // Only authenticated users can see the relationship
    (ctx) => !!ctx.auth.user,
    // Only admins or the post owner can see user details
    (ctx, app, post) => {
      return ctx.auth.user?.role === 'admin' || 
             ctx.auth.user?.id === post.userId
    }
  ]
})
declare user: BelongsTo<typeof User>

Configuration Options

Lucid BelongsTo Options

All standard Lucid belongsTo relationship options are supported:

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'user_id',           // Foreign key column
  localKey: 'id',                  // Local key column (defaults to primary key)
  serializeAs: 'author',           // Serialization name
  onQuery: (query) => {            // Query modifications
    query.select('id', 'name', 'email')
  }
})
declare user: BelongsTo<typeof User>

Resourceful Relationship Options

readAccessControlFilters

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

Control who can access the relationship data:

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'user_id',
  readAccessControlFilters: [
    (ctx, app, instance) => {
      // Allow if user is the owner or an admin
      return ctx.auth.user?.id === instance.userId || 
             ctx.auth.user?.role === 'admin'
    }
  ]
})
declare user: BelongsTo<typeof User>

description

  • Type: string
  • Optional: Yes

Description for OpenAPI documentation:

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'user_id',
  description: 'The user who created this post'
})
declare user: BelongsTo<typeof User>

deprecated

  • Type: boolean
  • Optional: Yes

Mark the relationship as deprecated:

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'legacy_user_id',
  deprecated: true,
  description: 'Legacy user relationship - use author instead'
})
declare legacyUser: BelongsTo<typeof User>

externalDocs

  • Type: ExternalDocumentationObject
  • Optional: Yes

External documentation reference:

typescript
@resourcefulBelongsTo(() => User, {
  foreignKey: 'user_id',
  externalDocs: {
    description: 'User model documentation',
    url: 'https://docs.example.com/models/user'
  }
})
declare user: BelongsTo<typeof User>

Common Patterns

Author Relationship

typescript
class Post extends compose(BaseModel, withResourceful({ name: 'Post' })) {
  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare userId: number

  @resourcefulBelongsTo(() => User, {
    foreignKey: 'user_id',
    serializeAs: 'author',
    description: 'The author of this post',
    readAccessControlFilters: [
      // Authors can see their own user info, others see limited info
      (ctx, app, post) => {
        return ctx.auth.user?.id === post.userId
      }
    ],
    onQuery: (query) => {
      query.select('id', 'name', 'avatar_url')
    }
  })
  declare user: BelongsTo<typeof User>
}

Category Relationship

typescript
class Product extends compose(BaseModel, withResourceful({ name: 'Product' })) {
  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare categoryId: number

  @resourcefulBelongsTo(() => Category, {
    foreignKey: 'category_id',
    description: 'Product category',
    readAccessControlFilters: [
      // Only show active categories
      (ctx, app, product) => product.category?.isActive !== false
    ]
  })
  declare category: BelongsTo<typeof Category>
}

Soft Delete Aware Relationship

typescript
class Comment extends compose(BaseModel, withResourceful({ name: 'Comment' })) {
  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare postId: number

  @resourcefulBelongsTo(() => Post, {
    foreignKey: 'post_id',
    description: 'The post this comment belongs to',
    readAccessControlFilters: [
      // Don't show deleted posts
      (ctx, app, comment) => !comment.post?.deletedAt
    ],
    onQuery: (query) => {
      query.whereNull('deleted_at')
    }
  })
  declare post: BelongsTo<typeof Post>
}

Multi-tenant Relationship

typescript
class Order extends compose(BaseModel, withResourceful({ name: 'Order' })) {
  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare customerId: number

  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare tenantId: number

  @resourcefulBelongsTo(() => Customer, {
    foreignKey: 'customer_id',
    description: 'Customer who placed this order',
    readAccessControlFilters: [
      // Ensure same tenant access
      (ctx, app, order) => {
        return ctx.auth.user?.tenantId === order.tenantId
      }
    ],
    onQuery: (query) => {
      // Add tenant scoping to query
      query.where('tenant_id', (ctx) => ctx.auth.user?.tenantId)
    }
  })
  declare customer: BelongsTo<typeof Customer>
}

Advanced Examples

Conditional Relationship Loading

typescript
class Task extends compose(BaseModel, withResourceful({ name: 'Task' })) {
  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType(),
    nullable: true 
  })
  declare assigneeId: number | null

  @resourcefulBelongsTo(() => User, {
    foreignKey: 'assignee_id',
    serializeAs: 'assignee',
    description: 'User assigned to this task',
    readAccessControlFilters: [
      // Only show assignee if user has permission to see assignments
      (ctx, app, task) => {
        if (!task.assigneeId) return true // No assignee to hide
        
        return ctx.auth.user?.role === 'admin' ||
               ctx.auth.user?.id === task.assigneeId ||
               ctx.auth.user?.id === task.createdBy
      }
    ]
  })
  declare assignee: BelongsTo<typeof User>
}

Polymorphic-like Relationship

typescript
class Notification extends compose(BaseModel, withResourceful({ name: 'Notification' })) {
  @resourcefulColumn.string({ 
    type: ResourcefulStringType() 
  })
  declare actorType: string

  @resourcefulColumn.unsignedint({ 
    type: ResourcefulUnsignedIntegerType() 
  })
  declare actorId: number

  @resourcefulBelongsTo(() => User, {
    foreignKey: 'actor_id',
    description: 'User who triggered this notification',
    readAccessControlFilters: [
      // Only show if actor_type is 'user'
      (ctx, app, notification) => notification.actorType === 'user',
      // And user has permission to see the actor
      (ctx, app, notification) => {
        return ctx.auth.user?.role === 'admin' ||
               ctx.auth.user?.id === notification.userId
      }
    ]
  })
  declare actor: BelongsTo<typeof User>
}

Complete Example

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

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

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

  @resourcefulColumn.unsignedint({
    type: ResourcefulUnsignedIntegerType()
  })
  declare userId: number

  @resourcefulColumn.unsignedint({
    type: ResourcefulUnsignedIntegerType(),
    nullable: true
  })
  declare categoryId: number | null

  // Author relationship with access control
  @resourcefulBelongsTo(() => User, {
    foreignKey: 'user_id',
    serializeAs: 'author',
    description: 'The user who created this post',
    readAccessControlFilters: [
      // Always show author name and avatar
      (ctx) => !!ctx.auth.user,
      // Show full author details only to the author themselves or admins
      (ctx, app, post) => {
        return ctx.auth.user?.id === post.userId || 
               ctx.auth.user?.role === 'admin'
      }
    ],
    onQuery: (query) => {
      query.select('id', 'name', 'email', 'avatar_url', 'role')
    }
  })
  declare user: BelongsTo<typeof User>

  // Category relationship
  @resourcefulBelongsTo(() => Category, {
    foreignKey: 'category_id',
    description: 'Post category',
    readAccessControlFilters: [
      // Only show active categories
      (ctx, app, post) => {
        return !post.category || post.category.isActive
      }
    ],
    onQuery: (query) => {
      query.where('is_active', true)
    }
  })
  declare category: BelongsTo<typeof Category>

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

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

Best Practices

  1. Use descriptive foreign keys: Name foreign key columns clearly (user_id, not just id)
  2. Implement appropriate access control: Secure relationship data based on user permissions
  3. Use onQuery for performance: Limit selected columns and add necessary where clauses
  4. Handle null relationships: Consider nullable foreign keys and their implications
  5. Use serializeAs for clarity: Rename relationships for better API field names
  6. Add descriptions: Document what each relationship represents
  7. Consider query optimization: Use eager loading strategies appropriately
  8. Implement tenant scoping: Add tenant checks for multi-tenant applications
  9. Handle soft deletes: Exclude deleted related records when appropriate
  10. Use type safety: Leverage TypeScript for better development experience