Skip to content

Validation Middleware

The validation middleware provides content schema validation for the ReX content system. It ensures that content adheres to defined schemas before operations are executed, improving data consistency and preventing invalid content from entering the system.

Overview

Validation middleware intercepts content operations, particularly write operations, and validates content against a schema. It throws detailed validation errors when content doesn’t meet the schema requirements, providing clear feedback to fix validation issues.

typescript
import { withValidation } from '@lib/content/middleware'
import { contentSchema } from '@lib/content/schemas'

// Create validation middleware
const validationMiddleware = withValidation({
  schema: contentSchema,
  failOnError: true,
})

API Reference

withValidation

typescript
function withValidation(options: ValidationOptions): Middleware

Creates a new validation middleware instance.

Parameters:

  • options: Configuration options for validation

Returns:

  • A middleware function that performs content validation

ValidationOptions

typescript
interface ValidationOptions {
  /**
   * Schema to validate content against (required)
   */
  schema: ContentSchema

  /**
   * Whether to throw an error on validation failure (default: true)
   */
  failOnError?: boolean

  /**
   * Only validate certain operations (default: ['write'])
   */
  operations?: Array<'read' | 'write' | 'delete' | 'list'>

  /**
   * How to handle missing properties (default: 'error')
   */
  missingProperties?: 'error' | 'ignore' | 'warning'

  /**
   * Additional validation options
   */
  validationOptions?: {
    /**
     * Minimum allowed content size in bytes (optional)
     */
    minSize?: number

    /**
     * Maximum allowed content size in bytes (optional)
     */
    maxSize?: number

    /**
     * Custom validation function (optional)
     */
    customValidator?: (
      content: Content
    ) => ValidationResult | Promise<ValidationResult>
  }
}

Schema System

The validation middleware uses a flexible schema system that supports various schema formats:

JSON Schema

typescript
// JSON Schema for content
const jsonSchema = {
  type: 'object',
  required: ['data', 'contentType', 'metadata'],
  properties: {
    data: { type: 'string' },
    contentType: { type: 'string' },
    metadata: {
      type: 'object',
      properties: {
        title: { type: 'string' },
        description: { type: 'string' },
        tags: {
          type: 'array',
          items: { type: 'string' },
        },
      },
    },
  },
}

// Create validation middleware with JSON Schema
const jsonSchemaValidation = withValidation({
  schema: {
    type: 'json-schema',
    schema: jsonSchema,
  },
})

Zod Schema

typescript
import { z } from 'zod'

// Zod schema for content
const zodSchema = z.object({
  data: z.string(),
  contentType: z.string(),
  metadata: z.object({
    title: z.string().optional(),
    description: z.string().optional(),
    tags: z.array(z.string()).optional(),
  }),
})

// Create validation middleware with Zod
const zodValidation = withValidation({
  schema: {
    type: 'zod',
    schema: zodSchema,
  },
})

TypeScript Types (Runtime)

typescript
// TypeScript interface (with runtime validation)
interface ArticleContent extends Content {
  metadata: {
    title: string
    author: string
    publishDate?: Date
  }
}

// Type validation function
function isArticleContent(content: any): content is ArticleContent {
  return (
    typeof content.data === 'string' &&
    typeof content.contentType === 'string' &&
    typeof content.metadata === 'object' &&
    typeof content.metadata.title === 'string' &&
    typeof content.metadata.author === 'string'
  )
}

// Create validation middleware with type validation
const typeValidation = withValidation({
  schema: {
    type: 'custom',
    validate: content => {
      if (!isArticleContent(content)) {
        return {
          valid: false,
          errors: ['Content does not match ArticleContent type'],
        }
      }
      return { valid: true }
    },
  },
})

Validation Process

The validation middleware follows these steps when processing content:

  1. Check applicability: Determine if validation should run for the current operation
  2. Extract content: Get content from context based on operation type
  3. Run schema validation: Apply the schema to the content
  4. Handle validation results: Either throw an error or store results in context
  5. Continue pipeline: Pass control to the next middleware if validation passes
typescript
// Simplified implementation
function withValidation(options: ValidationOptions): Middleware {
  const { schema, failOnError = true, operations = ['write'] } = options

  return async (context, next) => {
    // Skip validation for non-applicable operations
    if (!operations.includes(context.operation)) {
      return next()
    }

    // Get content based on operation
    const content = context.content
    if (!content) {
      return next()
    }

    // Validate content
    const validationResult = await validateContent(content, schema)

    // Store validation result in context
    context.state.validationResult = validationResult

    // Handle validation failure
    if (!validationResult.valid && failOnError) {
      throw new ContentValidationError(
        `Content validation failed for ${context.uri}`,
        context.uri,
        'validation',
        true,
        {
          validationErrors: validationResult.errors,
          canRetry: true,
        }
      )
    }

    // Continue pipeline
    return next()
  }
}

Validation Results

Validation results are stored in the context state and include:

typescript
interface ValidationResult {
  /**
   * Whether validation passed
   */
  valid: boolean

  /**
   * Validation errors (if any)
   */
  errors?: string[]

  /**
   * Warning messages (non-blocking issues)
   */
  warnings?: string[]

  /**
   * Additional validation metadata
   */
  metadata?: Record<string, any>
}

Error Handling

The validation middleware throws a specialized error type when validation fails:

typescript
class ContentValidationError extends ContentError {
  constructor(
    message: string,
    path: string,
    source = 'validation',
    public validationErrors: string[]
  ) {
    super(
      message,
      path,
      source,
      true, // Recoverable error
      {
        canRetry: true,
        validationErrors,
        suggestedFixes: generateFixSuggestions(validationErrors),
      }
    )
    this.name = 'ContentValidationError'
  }
}

This error includes:

  • Validation Errors: Specific validation failures
  • Recovery Options: Suggestions on how to fix the errors
  • Retry Flag: Indication that the operation can be retried after fixing

Advanced Usage

Custom Validators

You can provide custom validation logic:

typescript
const customValidation = withValidation({
  schema: {
    type: 'custom',
    validate: async content => {
      // Implement custom validation logic
      const errors = []

      // Check required fields
      if (!content.metadata.title) {
        errors.push('Title is required')
      }

      // Perform complex validation
      if (
        content.contentType === 'text/markdown' &&
        !content.data.includes('# ')
      ) {
        errors.push('Markdown content must include at least one heading')
      }

      // External validation
      if (content.metadata.externalId) {
        const valid = await externalSystem.validateId(
          content.metadata.externalId
        )
        if (!valid) {
          errors.push('External ID is invalid')
        }
      }

      return {
        valid: errors.length === 0,
        errors: errors.length > 0 ? errors : undefined,
      }
    },
  },
})

Conditional Validation

You can apply different validation based on content type:

typescript
import {
  conditionalMiddleware,
  composeMiddleware,
} from '@lib/content/middleware'

const contentTypeValidation = composeMiddleware(
  // Apply markdown schema only to markdown content
  conditionalMiddleware(
    context => context.content?.contentType === 'text/markdown',
    withValidation({ schema: markdownSchema })
  ),

  // Apply JSON schema only to JSON content
  conditionalMiddleware(
    context => context.content?.contentType === 'application/json',
    withValidation({ schema: jsonSchema })
  ),

  // Default validation for all content types
  withValidation({
    schema: baseSchema,
    // Only validate fields not covered by specific schemas
    validationOptions: { ignoreCoveredFields: true },
  })
)

Integration with Content Processors

Validation can work with content processors:

typescript
import { composeMiddleware } from '@lib/content/middleware'

const processingPipeline = composeMiddleware(
  // First normalize content
  normalizationMiddleware(),

  // Then validate normalized content
  withValidation({ schema: contentSchema }),

  // Then transform content
  transformationMiddleware()
)

Performance Considerations

  • Schema Complexity: Complex schemas can impact performance
  • Validation Timing: By default, validation runs on write operations
  • Cache Validation Results: Results are stored in context.state for reuse
  • Selective Validation: Consider validating only on certain operations
  • Validation Bypass: Use conditionalMiddleware to skip validation when appropriate

Examples

Basic Content Validation

typescript
import { withValidation } from '@lib/content/middleware'
import { createContentStore } from '@lib/content'

// Define schema
const blogPostSchema = {
  type: 'json-schema',
  schema: {
    type: 'object',
    required: ['data', 'contentType', 'metadata'],
    properties: {
      data: { type: 'string' },
      contentType: {
        type: 'string',
        enum: ['text/markdown'],
      },
      metadata: {
        type: 'object',
        required: ['title', 'author'],
        properties: {
          title: { type: 'string', minLength: 3 },
          author: { type: 'string' },
          publishDate: { type: 'string', format: 'date-time' },
        },
      },
    },
  },
}

// Create store with validation
const store = createContentStore({
  adapter: createMemoryAdapter(),
  middleware: [withValidation({ schema: blogPostSchema })],
})

// Usage (will fail validation)
try {
  await store.write('blog/invalid-post.md', {
    data: '# Post',
    contentType: 'text/markdown',
    metadata: {
      // Missing required 'author' field
      title: 'My Post',
    },
  })
} catch (error) {
  console.error(error.message) // "Content validation failed for blog/invalid-post.md"
  console.error(error.validationErrors) // ["Missing required field: author"]
}

Progressive Validation

typescript
import { withValidation, conditionalMiddleware } from '@lib/content/middleware'

// Draft schema (less strict)
const draftSchema = {
  /* ... */
}

// Published schema (more strict)
const publishedSchema = {
  /* ... */
}

// Create progressive validation
const progressiveValidation = conditionalMiddleware(
  context => context.content?.metadata?.status === 'published',
  withValidation({ schema: publishedSchema, failOnError: true }),
  withValidation({ schema: draftSchema, failOnError: false })
)

Released under the MIT License.