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
$refreferences - 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
static async $asOpenApiSchemaObject(
ctx: HttpContext,
app: ApplicationService
): Promise<ResourcefulModelOpenApiSchema>Parameters
| Parameter | Type | Description |
|---|---|---|
ctx | HttpContext | HTTP context containing request information and authentication |
app | ApplicationService | Application 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 schemastitle- The model's resourceful namedescription- Optional model description from metadataproperties- Object containing schema definitions for accessible fieldsrequired- Array of required field names (non-nullable fields)externalDocs- Optional external documentation referenceexample- Optional example value for the schema
Basic Usage
Simple Schema Generation
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
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 metadataContext-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
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
// 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
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
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
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
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
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
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 referenceValidation Integration
OpenAPI schemas automatically incorporate validation rules defined in resourceful decorators and Joi validation schemas.
Field Validation Constraints
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
@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
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
@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
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:
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:
// 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
- Consistent Naming - Use clear, consistent model names that translate well to API documentation
- Comprehensive Descriptions - Add meaningful descriptions to models and fields
- Proper Examples - Provide realistic examples that help API consumers understand expected data
- External Documentation - Link to comprehensive guides and tutorials
Security Considerations
- ACL-Driven Schemas - Ensure schemas only expose data the current user can access
- Context Validation - Always pass the current request context to schema generation
- Sensitive Data Filtering - Use read/write access control to hide sensitive information
- Role-Based Schemas - Generate different schemas for different user roles
Performance Optimization
- Schema Caching - Cache generated schemas based on user permissions and context
- Lazy Loading - Generate schemas only when needed for documentation or validation
- Minimal Context - Use minimal request context for schema generation when possible
- Relationship Optimization - Control relationship inclusion based on actual usage patterns
Error Handling
Schema generation errors are handled gracefully with specific error types:
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
}
}Related Documentation
- 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: