Skip to content

@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

typescript
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:

typescript
@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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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.

typescript
@resourcefulComputed({ 
  type: ResourcefulStringType({ minLength: 1, maxLength: 255 })
})
get computedValue(): string {
  return this.calculateValue()
}

Lucid Computed Options

All standard Lucid computed options are supported:

typescript
@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.

typescript
@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.

typescript
@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.

typescript
@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:

typescript
@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

typescript
@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:

typescript
@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

typescript
// 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

typescript
@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

typescript
@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

typescript
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

  1. Keep computations lightweight: Avoid heavy calculations in getters that run frequently
  2. Use appropriate types: Choose the most specific ResourcefulDataType for return values
  3. Implement access control: Secure sensitive computed properties
  4. Provide descriptions: Add clear descriptions for API documentation
  5. Handle null cases: Be explicit about nullable return values
  6. Cache expensive operations: Consider caching for complex calculations
  7. Use relationship data carefully: Be mindful of N+1 queries when accessing relationships
  8. Consider serialization names: Use serializeAs for better API field naming
  9. Validate return types: Ensure computed values match their declared types
  10. Document side effects: Clearly document any side effects in computed properties