@resourcefulComputed
The @resourcefulComputed decorator enhances AdonisJS Lucid computed accessors with validation, access control, and metadata functionality. It provides both a generic decorator and type-specific variants for common data types.
For complete API reference, see resourcefulComputed.
Overview
The @resourcefulComputed decorator combines the functionality of Lucid's @computed decorator with resourceful features:
- Type Validation: Joi schema validation for computed property return values
- Access Control: Field-level read/write permissions
- OpenAPI Integration: Schema generation for API documentation
- Metadata Storage: Rich metadata for runtime introspection
- Serialization Control: Control how computed values appear in responses
Basic Usage
Generic Computed Accessor
import { resourcefulComputed } from '@nhtio/lucid-resourceful'
import { ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'
class User extends compose(BaseModel, withResourceful({ name: 'User' })) {
@resourcefulColumn.string({ type: ResourcefulStringType() })
declare firstName: string
@resourcefulColumn.string({ type: ResourcefulStringType() })
declare lastName: string
@resourcefulComputed({
type: ResourcefulStringType({ minLength: 1, maxLength: 200 }),
nullable: false,
description: 'User\'s full name'
})
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}
}Read-Only Computed Properties
Computed accessors are inherently read-only but can still have access control:
@resourcefulComputed.string({
type: ResourcefulStringType(),
readAccessControlFilters: [
(ctx, app, instance) => ctx.auth.user?.id === instance?.id || ctx.auth.user?.role === 'admin'
]
})
get sensitiveInfo(): string {
return this.calculateSensitiveValue()
}Type-Specific Variants
String Computed Accessors
// Display name formatting
@resourcefulComputed.string({
type: ResourcefulStringType({ maxLength: 100 }),
description: 'Formatted display name'
})
get displayName(): string {
return this.name.toUpperCase()
}
// Email display with masking
@resourcefulComputed.string({
type: ResourcefulStringType(),
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user?.id
]
})
get maskedEmail(): string {
const [local, domain] = this.email.split('@')
return `${local.slice(0, 2)}***@${domain}`
}Numeric Computed Accessors
// Age calculation
@resourcefulComputed.integer({
type: ResourcefulIntegerType({ min: 0, max: 150 }),
description: 'Calculated age in years'
})
get age(): number {
return DateTime.now().diff(this.birthDate, 'years').years
}
// Price with discount
@resourcefulComputed.number({
type: ResourcefulNumberType({ min: 0 }),
description: 'Price after applying discount'
})
get discountedPrice(): number {
return this.price * (1 - this.discountPercent / 100)
}
// Total count from relationship
@resourcefulComputed.unsignedint({
type: ResourcefulUnsignedIntegerType({ min: 0 })
})
get postCount(): number {
return this.posts?.length || 0
}Boolean Computed Accessors
// Status checks
@resourcefulComputed.boolean({
type: ResourcefulBooleanType(),
description: 'Whether the user is currently active'
})
get isActive(): boolean {
return this.status === 'active' && this.emailVerified
}
// Permission checks
@resourcefulComputed.boolean({
type: ResourcefulBooleanType(),
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user?.id || ctx.auth.user?.role === 'admin'
]
})
get canEdit(): boolean {
return this.role === 'admin' || this.ownerId === this.id
}Date and DateTime Computed Accessors
// Next scheduled date
@resourcefulComputed.date({
type: ResourcefulDateType(),
nullable: true
})
get nextScheduledDate(): Date | null {
return this.calculateNextDate()
}
// Last activity timestamp
@resourcefulComputed.dateTime({
type: ResourcefulDateTimeType(),
nullable: true,
description: 'Last recorded activity timestamp'
})
get lastActivity(): DateTime | null {
return this.activities?.[0]?.createdAt || null
}Object and Array Computed Accessors
// Formatted address object
@resourcefulComputed.object({
type: ResourcefulObjectType({
properties: {
street: { type: 'string' },
city: { type: 'string' },
country: { type: 'string' }
}
}),
nullable: true
})
get formattedAddress(): Record<string, string> | null {
if (!this.address) return null
return {
street: this.streetAddress,
city: this.city,
country: this.country
}
}
// Recent activity array
@resourcefulComputed.array({
type: ResourcefulArrayType({
items: { type: 'object' }
}),
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user?.id
]
})
get recentActivities(): any[] {
return this.activities?.slice(0, 5) || []
}Configuration Options
Required Options
type
- Type:
ResourcefulDataType - Required: Yes
The ResourcefulDataType instance that defines the computed property's return type and validation. See Data Type Definitions for available types and their configuration options.
@resourcefulComputed({
type: ResourcefulStringType({ minLength: 1, maxLength: 255 })
})
get computedValue(): string {
return this.calculateValue()
}Lucid Computed Options
All standard Lucid computed options are supported:
@resourcefulComputed({
type: ResourcefulStringType(),
serializeAs: 'custom_name', // Serialization name
meta: { custom: 'metadata' } // Custom metadata
})
get value(): string {
return this.internalValue
}Resourceful Options
nullable
- Type:
boolean - Default:
false
Whether the computed property can return null.
@resourcefulComputed.string({
type: ResourcefulStringType(),
nullable: true
})
get optionalValue(): string | null {
return this.condition ? this.value : null
}description
- Type:
string - Optional: Yes
Description for OpenAPI documentation.
@resourcefulComputed.string({
type: ResourcefulStringType(),
description: 'User\'s full formatted name'
})
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}example
- Type:
string - Optional: Yes
Example value for OpenAPI documentation.
@resourcefulComputed.string({
type: ResourcefulStringType(),
example: 'John Doe'
})
get displayName(): string {
return this.formatName()
}Access Control
Read Access Control
Control who can access the computed property:
@resourcefulComputed.string({
type: ResourcefulStringType(),
readAccessControlFilters: [
// Only the user or admins can see personal info
(ctx, app, instance) => ctx.auth.user?.id === instance?.id || ctx.auth.user?.role === 'admin'
]
})
get personalSummary(): string {
return this.generatePersonalSummary()
}Multiple Access Filters
@resourcefulComputed.number({
type: ResourcefulNumberType(),
readAccessControlFilters: [
// Must be authenticated
(ctx) => !!ctx.auth.user,
// Must have permission
(ctx, app, instance) => this.checkPermission(ctx.auth.user, instance),
// Must be in same tenant
(ctx, app, instance) => ctx.auth.user?.tenantId === instance?.tenantId
]
})
get sensitiveCalculation(): number {
return this.performSensitiveCalculation()
}Validation Scoping
Dynamically modify validation based on context:
@resourcefulComputed.string({
type: ResourcefulStringType(),
validationScoper: (schema, ctx, operation) => {
if (ctx.auth.user?.role === 'admin') {
// Admins see more detailed information
return schema.max(1000)
}
return schema.max(100)
}
})
get statusMessage(): string {
if (this.ctx?.auth.user?.role === 'admin') {
return this.getDetailedStatus()
}
return this.getBasicStatus()
}Advanced Examples
Relationship-Based Computed Properties
// Count from relationships
@resourcefulComputed.unsignedint({
type: ResourcefulUnsignedIntegerType({ min: 0 }),
description: 'Total number of published posts'
})
get publishedPostsCount(): number {
return this.posts?.filter(post => post.isPublished).length || 0
}
// Aggregated data
@resourcefulComputed.number({
type: ResourcefulNumberType({ min: 0 }),
description: 'Average rating from all reviews'
})
get averageRating(): number {
if (!this.reviews?.length) return 0
const total = this.reviews.reduce((sum, review) => sum + review.rating, 0)
return total / this.reviews.length
}Conditional Computed Properties
@resourcefulComputed.string({
type: ResourcefulStringType(),
nullable: true,
readAccessControlFilters: [
(ctx, app, user) => user?.isPublic || ctx.auth.user?.id === user?.id
]
})
get profileStatus(): string | null {
if (!this.isPublic && this.ctx?.auth.user?.id !== this.id) {
return null
}
return this.calculateProfileStatus()
}Complex Object Computed Properties
@resourcefulComputed.object({
type: ResourcefulObjectType({
properties: {
summary: { type: 'string' },
stats: {
type: 'object',
properties: {
posts: { type: 'number' },
followers: { type: 'number' },
following: { type: 'number' }
}
},
permissions: {
type: 'array',
items: { type: 'string' }
}
}
}),
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user?.id || ctx.auth.user?.role === 'admin'
]
})
get userProfile(): Record<string, any> {
return {
summary: this.generateSummary(),
stats: {
posts: this.posts?.length || 0,
followers: this.followers?.length || 0,
following: this.following?.length || 0
},
permissions: this.calculatePermissions()
}
}Complete Example
import { DateTime } from 'luxon'
import { BaseModel } from '@adonisjs/lucid/orm'
import { compose } from '@adonisjs/core/helpers'
import { withResourceful, resourcefulColumn, resourcefulComputed } from '@nhtio/lucid-resourceful'
import {
ResourcefulStringType,
ResourcefulIntegerType,
ResourcefulBooleanType,
ResourcefulDateTimeType,
ResourcefulObjectType
} from '@nhtio/lucid-resourceful/definitions'
export default class User extends compose(BaseModel, withResourceful({
name: 'User'
})) {
@resourcefulColumn.string({ type: ResourcefulStringType() })
declare firstName: string
@resourcefulColumn.string({ type: ResourcefulStringType() })
declare lastName: string
@resourcefulColumn.date({ type: ResourcefulDateType() })
declare birthDate: Date
@resourcefulColumn.string({ type: ResourcefulStringType() })
declare status: string
// Full name computed property
@resourcefulComputed.string({
type: ResourcefulStringType({ minLength: 1, maxLength: 200 }),
description: 'User\'s full name',
example: 'John Doe'
})
get fullName(): string {
return `${this.firstName} ${this.lastName}`.trim()
}
// Age calculation
@resourcefulComputed.integer({
type: ResourcefulIntegerType({ min: 0, max: 150 }),
description: 'Current age in years'
})
get age(): number {
return DateTime.now().diff(DateTime.fromJSDate(this.birthDate), 'years').years
}
// Status check
@resourcefulComputed.boolean({
type: ResourcefulBooleanType(),
description: 'Whether the user is currently active'
})
get isActive(): boolean {
return this.status === 'active'
}
// Personal profile (access controlled)
@resourcefulComputed.object({
type: ResourcefulObjectType(),
readAccessControlFilters: [
(ctx, app, user) => ctx.auth.user?.id === user?.id || ctx.auth.user?.role === 'admin'
],
description: 'Detailed user profile information'
})
get personalProfile(): Record<string, any> {
return {
name: this.fullName,
age: this.age,
status: this.status,
lastLogin: this.lastLoginAt,
preferences: this.preferences
}
}
// Public profile (limited info)
@resourcefulComputed.object({
type: ResourcefulObjectType(),
description: 'Public profile information'
})
get publicProfile(): Record<string, any> {
return {
name: this.fullName,
isActive: this.isActive
}
}
}Best Practices
- Keep computations lightweight: Avoid heavy calculations in getters that run frequently
- Use appropriate types: Choose the most specific ResourcefulDataType for return values
- Implement access control: Secure sensitive computed properties
- Provide descriptions: Add clear descriptions for API documentation
- Handle null cases: Be explicit about nullable return values
- Cache expensive operations: Consider caching for complex calculations
- Use relationship data carefully: Be mindful of N+1 queries when accessing relationships
- Consider serialization names: Use
serializeAsfor better API field naming - Validate return types: Ensure computed values match their declared types
- Document side effects: Clearly document any side effects in computed properties