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.
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
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
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
// 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
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 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:
- Check applicability: Determine if validation should run for the current operation
- Extract content: Get content from context based on operation type
- Run schema validation: Apply the schema to the content
- Handle validation results: Either throw an error or store results in context
- Continue pipeline: Pass control to the next middleware if validation passes
// 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:
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:
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:
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:
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:
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
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
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 })
)