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