Skip to content

OpenAPI Functionality

Lucid Resourceful provides comprehensive OpenAPI 3.0 schema generation capabilities that automatically create accurate API documentation based on your model definitions, access control rules, and validation schemas. The generated schemas reflect the actual data structure and constraints that users can expect when interacting with your API.

Overview

The OpenAPI functionality in Lucid Resourceful offers:

  • Context-Aware Schema Generation - Schemas reflect actual accessible fields based on user permissions
  • Automatic Type Mapping - Resourceful data types automatically convert to proper OpenAPI schemas
  • Relationship Support - Handles model relationships with $ref references
  • Validation Integration - Incorporates Joi validation rules into schema constraints
  • Access Control Filtering - Only includes fields the current user can access
  • Comprehensive Metadata - Supports descriptions, examples, external documentation, and more

Core Method: $asOpenApiSchemaObject

The primary method for OpenAPI schema generation is $asOpenApiSchemaObject, available on all ResourcefulModel instances.

Method Signature

typescript
static async $asOpenApiSchemaObject(
  ctx: HttpContext,
  app: ApplicationService
): Promise<ResourcefulModelOpenApiSchema>

Parameters

ParameterTypeDescription
ctxHttpContextHTTP context containing request information and authentication
appApplicationServiceApplication service instance for accessing app-level services

Return Value

Returns a Promise<ResourcefulModelOpenApiSchema> containing a complete OpenAPI 3.0 schema object with:

  • type - Always 'object' for model schemas
  • title - The model's resourceful name
  • description - Optional model description from metadata
  • properties - Object containing schema definitions for accessible fields
  • required - Array of required field names (non-nullable fields)
  • externalDocs - Optional external documentation reference
  • example - Optional example value for the schema

Basic Usage

Simple Schema Generation

typescript
import { HttpContext } from '@adonisjs/core/http'
import { ApplicationService } from '@adonisjs/core/types'

// Generate schema for current request context
const schema = await User.$asOpenApiSchemaObject(ctx, app)

console.log(schema)
// Output:
{
  type: 'object',
  title: 'User',
  properties: {
    id: { type: 'number', readOnly: true },
    name: { type: 'string' },
    email: { type: 'string', format: 'email' }
  },
  required: ['id', 'name', 'email']
}

Model with Metadata

typescript
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
import { withResourceful, resourcefulColumn } from 'lucid-resourceful'
import { ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'

class User extends withResourceful({
  name: 'User',
  description: 'System user with authentication and profile information',
  externalDocs: {
    description: 'User API Documentation',
    url: 'https://api.example.com/docs/users'
  },
  example: 'A typical user in the system'
})(BaseModel) {
  @column({ isPrimary: true })
  @resourcefulColumn({
    type: ResourcefulStringType(),
    description: 'Unique identifier for the user'
  })
  declare id: number

  @column()
  @resourcefulColumn({
    type: ResourcefulStringType({ 
      minLength: 2, 
      maxLength: 100 
    }),
    description: 'User\'s full name'
  })
  declare name: string

  @column()
  @resourcefulColumn({
    type: ResourcefulStringType({ 
      format: 'email' 
    }),
    description: 'User\'s email address'
  })
  declare email: string
}

// Generated schema includes metadata
const schema = await User.$asOpenApiSchemaObject(ctx, app)
// Result includes description, externalDocs, and field-level metadata

Context-Aware Schema Generation

One of the most powerful features is context-aware schema generation, where the schema reflects only the fields the current user can access based on ACL permissions.

Field Filtering by Permissions

typescript
class User extends withResourceful({
  name: 'User'
})(BaseModel) {
  @column({ isPrimary: true })
  @resourcefulColumn({
    type: ResourcefulStringType(),
    readAccessControlFilters: [() => true], // Always readable
  })
  declare id: number

  @column()
  @resourcefulColumn({
    type: ResourcefulStringType(),
    readAccessControlFilters: [() => true], // Always readable
  })
  declare name: string

  @column()
  @resourcefulColumn({
    type: ResourcefulStringType(),
    readAccessControlFilters: [
      // Only readable by admin users
      (ctx) => ctx.auth.user?.isAdmin === true
    ]
  })
  declare socialSecurityNumber: string
}

// Schema for admin user includes all fields
const adminSchema = await User.$asOpenApiSchemaObject(adminCtx, app)
// { properties: { id, name, socialSecurityNumber }, ... }

// Schema for regular user excludes sensitive fields
const userSchema = await User.$asOpenApiSchemaObject(userCtx, app)
// { properties: { id, name }, ... }

Dynamic Schema Based on Request Context

typescript
// Different schemas for different user types
class Document extends withResourceful({
  name: 'Document'
})(BaseModel) {
  @column()
  @resourcefulColumn({
    type: ResourcefulStringType(),
    readAccessControlFilters: [
      (ctx) => ctx.auth.user?.role === 'owner' || ctx.auth.user?.role === 'editor'
    ]
  })
  declare title: string

  @column()
  @resourcefulColumn({
    type: ResourcefulStringType(),
    readAccessControlFilters: [
      (ctx) => ctx.auth.user?.role === 'owner'
    ]
  })
  declare privateNotes: string
}

// Owner sees all fields
const ownerSchema = await Document.$asOpenApiSchemaObject(ownerCtx, app)

// Editor sees limited fields  
const editorSchema = await Document.$asOpenApiSchemaObject(editorCtx, app)

// Viewer sees minimal fields
const viewerSchema = await Document.$asOpenApiSchemaObject(viewerCtx, app)

Data Type Mapping

Resourceful data types automatically map to appropriate OpenAPI 3.0 schemas with proper constraints and formatting.

String Types

typescript
import { ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'

@resourcefulColumn({
  type: ResourcefulStringType({
    minLength: 3,
    maxLength: 50,
    pattern: '^[a-zA-Z0-9]+$',
    format: 'username'
  })
})
declare username: string

// Generated OpenAPI schema:
{
  type: 'string',
  minLength: 3,
  maxLength: 50,
  pattern: '^[a-zA-Z0-9]+$',
  format: 'username'
}

Number Types

typescript
import { ResourcefulNumberType } from '@nhtio/lucid-resourceful/definitions'

@resourcefulColumn({
  type: ResourcefulNumberType({
    minimum: 0,
    maximum: 100,
    multipleOf: 0.5,
    exclusiveMinimum: true
  })
})
declare score: number

// Generated OpenAPI schema:
{
  type: 'number',
  minimum: 0,
  maximum: 100,
  multipleOf: 0.5,
  exclusiveMinimum: true
}

Array Types

typescript
import { ResourcefulArrayType, ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'

@resourcefulColumn({
  type: ResourcefulArrayType({
    items: ResourcefulStringType(),
    minItems: 1,
    maxItems: 5,
    uniqueItems: true
  })
})
declare tags: string[]

// Generated OpenAPI schema:
{
  type: 'array',
  items: { type: 'string' },
  minItems: 1,
  maxItems: 5,
  uniqueItems: true
}

Object Types

typescript
import { 
  ResourcefulObjectType, 
  ResourcefulStringType, 
  ResourcefulNumberType 
} from '@nhtio/lucid-resourceful/definitions'

@resourcefulColumn({
  type: ResourcefulObjectType({
    properties: {
      street: ResourcefulStringType({ minLength: 1 }),
      city: ResourcefulStringType({ minLength: 1 }),
      zipCode: ResourcefulStringType({ pattern: '^\\d{5}$' }),
      coordinates: ResourcefulObjectType({
        properties: {
          lat: ResourcefulNumberType({ minimum: -90, maximum: 90 }),
          lng: ResourcefulNumberType({ minimum: -180, maximum: 180 })
        },
        required: ['lat', 'lng']
      })
    },
    required: ['street', 'city', 'zipCode'],
    additionalProperties: false
  })
})
declare address: object

// Generated OpenAPI schema:
{
  type: 'object',
  properties: {
    street: { type: 'string', minLength: 1 },
    city: { type: 'string', minLength: 1 },
    zipCode: { type: 'string', pattern: '^\\d{5}$' },
    coordinates: {
      type: 'object',
      properties: {
        lat: { type: 'number', minimum: -90, maximum: 90 },
        lng: { type: 'number', minimum: -180, maximum: 180 }
      },
      required: ['lat', 'lng']
    }
  },
  required: ['street', 'city', 'zipCode'],
  additionalProperties: false
}

Relationship Handling

Lucid Resourceful automatically handles model relationships in OpenAPI schemas using $ref references to related models.

Basic Relationship Schema

typescript
import { hasMany } from '@ioc:Adonis/Lucid/Orm'
import { resourcefulHasMany } from 'lucid-resourceful'

class User extends withResourceful({ name: 'User' })(BaseModel) {
  // ... other properties

  @hasMany(() => Post)
  @resourcefulHasMany(() => Post, {
    readAccessControlFilters: [
      (ctx) => ctx.auth.user?.id === ctx.params.id
    ]
  })
  declare posts: HasMany<typeof Post>
}

class Post extends withResourceful({ name: 'Post' })(BaseModel) {
  // ... post properties
}

// Generated User schema includes reference to Post
const userSchema = await User.$asOpenApiSchemaObject(ctx, app)
// Result:
{
  type: 'object',
  title: 'User',
  properties: {
    // ... other properties
    posts: { $ref: '#/components/schemas/Post' }
  }
}

Conditional Relationship Access

typescript
class Order extends withResourceful({ name: 'Order' })(BaseModel) {
  @belongsTo(() => User)
  @resourcefulBelongsTo(() => User, {
    readAccessControlFilters: [
      // Only include customer info for admin users or order owner
      (ctx) => ctx.auth.user?.isAdmin || ctx.auth.user?.id === ctx.params.userId
    ]
  })
  declare customer: BelongsTo<typeof User>

  @hasMany(() => OrderItem)
  @resourcefulHasMany(() => OrderItem, {
    readAccessControlFilters: [() => true] // Always include order items
  })
  declare items: HasMany<typeof OrderItem>
}

// Admin schema includes customer reference
const adminSchema = await Order.$asOpenApiSchemaObject(adminCtx, app)
// { properties: { customer: { $ref: '#/components/schemas/User' }, items: ... } }

// Customer schema excludes customer reference for other users' orders
const customerSchema = await Order.$asOpenApiSchemaObject(customerCtx, app)
// { properties: { items: ... } } - no customer reference

Validation Integration

OpenAPI schemas automatically incorporate validation rules defined in resourceful decorators and Joi validation schemas.

Field Validation Constraints

typescript
import { ResourcefulStringType } from '@nhtio/lucid-resourceful/definitions'
import { joi } from '@nhtio/lucid-resourceful/joi'

@resourcefulColumn({
  type: ResourcefulStringType({ 
    minLength: 6, 
    maxLength: 100 
  }),
  validate: {
    create: joi.string().email().required(),
    update: joi.string().email().optional()
  }
})
declare email: string

// Generated schema includes both type constraints and validation rules
{
  type: 'string',
  format: 'email',
  minLength: 6,
  maxLength: 100
}

Nullable Fields and Required Detection

typescript
@resourcefulColumn({
  type: ResourcefulStringType(),
  nullable: false  // Makes field required in schema
})
declare requiredField: string

@resourcefulColumn({
  type: ResourcefulStringType(),
  nullable: true   // Makes field optional and nullable
})
declare optionalField: string | null

// Generated schema:
{
  type: 'object',
  properties: {
    requiredField: { type: 'string' },
    optionalField: { type: 'string', nullable: true }
  },
  required: ['requiredField'] // Only non-nullable fields are required
}

Advanced Schema Features

Computed Accessors

typescript
import { computed } from '@ioc:Adonis/Lucid/Orm'
import { resourcefulComputed } from 'lucid-resourceful'

class User extends withResourceful({ name: 'User' })(BaseModel) {
  @column()
  @resourcefulColumn({ type: ResourcefulStringType() })
  declare firstName: string

  @column()
  @resourcefulColumn({ type: ResourcefulStringType() })
  declare lastName: string

  @computed()
  @resourcefulComputed({
    type: ResourcefulStringType(),
    description: 'User\'s full name (computed from first and last name)'
  })
  get fullName(): string {
    return `${this.firstName} ${this.lastName}`
  }
}

// Generated schema includes computed property
{
  type: 'object',
  properties: {
    firstName: { type: 'string' },
    lastName: { type: 'string' },
    fullName: { 
      type: 'string', 
      readOnly: true,
      description: 'User\'s full name (computed from first and last name)'
    }
  }
}

Read-Only and Write-Only Fields

typescript
@resourcefulColumn({
  type: ResourcefulStringType(),
  readOnly: true  // Field appears in responses but not in request schemas
})
declare createdAt: string

@resourcefulColumn({
  type: ResourcefulStringType(),
  writeOnly: true  // Field accepts input but doesn't appear in responses
})
declare password: string

// In response schemas:
{
  properties: {
    createdAt: { type: 'string', readOnly: true }
    // password field excluded from response schema
  }
}

// In request schemas:
{
  properties: {
    password: { type: 'string', writeOnly: true }
    // createdAt field excluded from request schema  
  }
}

External Documentation and Examples

typescript
class User extends withResourceful({
  name: 'User',
  description: 'A user account in the system',
  externalDocs: {
    description: 'User Management API Guide',
    url: 'https://docs.example.com/api/users'
  },
  example: 'Example user with complete profile information'
})(BaseModel) {
  @resourcefulColumn({
    type: ResourcefulStringType(),
    description: 'User\'s email address for login and communication',
    example: 'user@example.com'
  })
  declare email: string
}

// Generated schema includes all metadata
{
  type: 'object',
  title: 'User',
  description: 'A user account in the system',
  externalDocs: {
    description: 'User Management API Guide',
    url: 'https://docs.example.com/api/users'
  },
  example: 'Example user with complete profile information',
  properties: {
    email: {
      type: 'string',
      description: 'User\'s email address for login and communication',
      example: 'user@example.com'
    }
  }
}

Performance Considerations

Schema Caching

Since OpenAPI schema generation involves ACL evaluation, consider implementing caching strategies:

typescript
import cache from '@adonisjs/cache/services/main'

class SchemaCache {
  async getSchema(
    model: typeof ResourcefulModel,
    userId: string,
    userRole: string,
    ctx: HttpContext,
    app: ApplicationService
  ) {
    const key = `openapi:schema:${model.name}:${userId}:${userRole}`
    
    // Try to get cached schema
    const cachedSchema = await cache.get(key)
    if (cachedSchema) {
      return cachedSchema
    }

    // Generate new schema and cache it
    const schema = await model.$asOpenApiSchemaObject(ctx, app)
    await cache.set(key, schema, '5m') // Cache for 5 minutes
    
    return schema
  }

  async clearModelCache(modelName: string) {
    // Clear all cached schemas for a specific model
    await cache.deleteMany([`openapi:schema:${modelName}:*`])
  }

  async clearUserCache(userId: string) {
    // Clear all cached schemas for a specific user
    await cache.deleteMany([`openapi:schema:*:${userId}:*`])
  }
}

Selective Schema Generation

For large models, consider generating schemas only for required endpoints:

typescript
// Generate different schemas for different operations
async function getCreateUserSchema(ctx: HttpContext, app: ApplicationService) {
  // Context with write permissions for create operation
  return await User.$asOpenApiSchemaObject(createCtx, app)
}

async function getReadUserSchema(ctx: HttpContext, app: ApplicationService) {
  // Context with read permissions for response schemas
  return await User.$asOpenApiSchemaObject(readCtx, app)
}

Best Practices

Schema Organization

  1. Consistent Naming - Use clear, consistent model names that translate well to API documentation
  2. Comprehensive Descriptions - Add meaningful descriptions to models and fields
  3. Proper Examples - Provide realistic examples that help API consumers understand expected data
  4. External Documentation - Link to comprehensive guides and tutorials

Security Considerations

  1. ACL-Driven Schemas - Ensure schemas only expose data the current user can access
  2. Context Validation - Always pass the current request context to schema generation
  3. Sensitive Data Filtering - Use read/write access control to hide sensitive information
  4. Role-Based Schemas - Generate different schemas for different user roles

Performance Optimization

  1. Schema Caching - Cache generated schemas based on user permissions and context
  2. Lazy Loading - Generate schemas only when needed for documentation or validation
  3. Minimal Context - Use minimal request context for schema generation when possible
  4. Relationship Optimization - Control relationship inclusion based on actual usage patterns

Error Handling

Schema generation errors are handled gracefully with specific error types:

typescript
import { errors } from '@nhtio/lucid-resourceful'

try {
  const schema = await User.$asOpenApiSchemaObject(ctx, app)
} catch (error) {
  if (errors.isInstanceOf.E_FORBIDDEN(error)) {
    // Handle access control errors
    console.log('User lacks permission to access this schema')
  } else if (errors.isInstanceOf.E_MISSING_PRIMARY_KEY_EXCEPTION(error)) {
    // Handle model configuration errors
    console.log('Model is missing required primary key')
  } else {
    // Handle other errors
    throw error
  }
}
  • Data Type Definitions - Complete guide to resourceful data types and their OpenAPI mappings
  • Decorators - Learn how to define resourceful fields with proper metadata
  • CRUD Operations - Understanding how CRUD operations use generated schemas
  • Validation - Comprehensive validation system integration with OpenAPI
  • Error Handling - Complete error handling reference for schema generation

API Reference

For complete type definitions and method signatures, see: