diff --git a/packages/objectql/src/protocol.ts b/packages/objectql/src/protocol.ts index 5a6fbc96a..1633a5cd0 100644 --- a/packages/objectql/src/protocol.ts +++ b/packages/objectql/src/protocol.ts @@ -59,17 +59,29 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol { } async getMetaItems(request: { type: string; packageId?: string }) { + let items = SchemaRegistry.listItems(request.type, request.packageId); + // Normalize singular/plural: REST uses singular ('app') but registry may store as plural ('apps') + if (items.length === 0) { + const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's'; + items = SchemaRegistry.listItems(alt, request.packageId); + } return { type: request.type, - items: SchemaRegistry.listItems(request.type, request.packageId) + items }; } async getMetaItem(request: { type: string, name: string }) { + let item = SchemaRegistry.getItem(request.type, request.name); + // Normalize singular/plural + if (item === undefined) { + const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's'; + item = SchemaRegistry.getItem(alt, request.name); + } return { type: request.type, name: request.name, - item: SchemaRegistry.getItem(request.type, request.name) + item }; } diff --git a/packages/spec/REST_API_PLUGIN.md b/packages/spec/REST_API_PLUGIN.md new file mode 100644 index 000000000..b9eab7587 --- /dev/null +++ b/packages/spec/REST_API_PLUGIN.md @@ -0,0 +1,462 @@ +# REST API Plugin Implementation + +## Overview + +This document describes the implementation of Phase 2 of the API Protocol plan: **核心 REST API 插件** (Core REST API Plugin). + +The REST API plugin provides a standardized way to register and configure REST API endpoints for ObjectStack services, with built-in support for: +- Request validation using Zod schemas +- Response envelope wrapping +- Standardized error handling +- OpenAPI documentation auto-generation + +## Key Components + +### 1. Route Registration (`RestApiRouteRegistrationSchema`) + +Defines how to register groups of related endpoints under a common prefix. + +**Example:** +```typescript +const dataRoutes: RestApiRouteRegistration = { + prefix: '/api/v1/data', + service: 'data', + category: 'data', + methods: ['findData', 'getData', 'createData', 'updateData', 'deleteData'], + endpoints: [ + { + method: 'GET', + path: '/:object', + handler: 'findData', + category: 'data', + public: false, + permissions: ['data.read'], + summary: 'Query records', + requestSchema: 'FindDataRequestSchema', + responseSchema: 'ListRecordResponseSchema', + }, + // ... more endpoints + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], + authRequired: true, +}; +``` + +### 2. Request Validation (`RequestValidationConfigSchema`) + +Configures automatic request validation using Zod schemas. + +**Features:** +- Three validation modes: `strict`, `permissive`, `strip` +- Validates body, query parameters, URL parameters, and headers +- Field-level error details +- Custom error messages + +**Example:** +```typescript +const validation: RequestValidationConfig = { + enabled: true, + mode: 'strict', + validateBody: true, + validateQuery: true, + validateParams: true, + includeFieldErrors: true, +}; +``` + +### 3. Response Envelope (`ResponseEnvelopeConfigSchema`) + +Standardizes all API responses using `BaseResponseSchema`. + +**Features:** +- Automatic wrapping of response data +- Metadata injection (timestamp, requestId, duration, traceId) +- Custom metadata support +- Skip wrapping for already-wrapped responses + +**Example:** +```typescript +const responseEnvelope: ResponseEnvelopeConfig = { + enabled: true, + includeMetadata: true, + includeTimestamp: true, + includeRequestId: true, + includeDuration: true, + includeTraceId: true, +}; +``` + +**Response Format:** +```json +{ + "success": true, + "data": { ... }, + "meta": { + "timestamp": "2026-02-08T10:00:00Z", + "requestId": "req_123", + "duration": 45, + "traceId": "trace_456" + } +} +``` + +### 4. Error Handling (`ErrorHandlingConfigSchema`) + +Standardizes error responses using `ApiErrorSchema`. + +**Features:** +- Stack trace inclusion (dev mode) +- Error logging +- Documentation URL generation +- Custom error messages by code +- Field redaction for sensitive data + +**Example:** +```typescript +const errorHandling: ErrorHandlingConfig = { + enabled: true, + includeStackTrace: false, + logErrors: true, + exposeInternalErrors: false, + includeDocumentation: true, + documentationBaseUrl: 'https://docs.objectstack.dev/errors', + customErrorMessages: { + validation_error: 'Your request data is invalid. Please check your input.', + }, + redactFields: ['password', 'ssn', 'creditCard'], +}; +``` + +**Error Response Format:** +```json +{ + "success": false, + "error": { + "code": "validation_error", + "message": "Validation failed for 2 fields", + "category": "validation", + "httpStatus": 400, + "retryable": false, + "fieldErrors": [ + { + "field": "email", + "code": "invalid_format", + "message": "Email format is invalid", + "value": "not-an-email" + } + ], + "timestamp": "2026-02-08T10:00:00Z", + "requestId": "req_123", + "documentation": "https://docs.objectstack.dev/errors/validation_error" + } +} +``` + +### 5. OpenAPI Generation (`OpenApiGenerationConfigSchema`) + +Automatically generates OpenAPI documentation from route definitions and Zod schemas. + +**Features:** +- OpenAPI 3.0.x and 3.1.0 support +- Multiple UI frameworks (Swagger UI, Redoc, RapiDoc, Elements) +- Auto-generated schemas from Zod definitions +- Request/response examples +- Server URLs, contact info, license info + +**Example:** +```typescript +const openApi: OpenApiGenerationConfig = { + enabled: true, + version: '3.0.3', + title: 'ObjectStack API', + description: 'Comprehensive API for ObjectStack', + apiVersion: '1.0.0', + outputPath: '/api/docs/openapi.json', + uiPath: '/api/docs', + uiFramework: 'swagger-ui', + generateSchemas: true, + includeExamples: true, + servers: [ + { url: 'https://api.example.com', description: 'Production' }, + { url: 'https://api-staging.example.com', description: 'Staging' }, + ], + contact: { + name: 'API Support', + email: 'api@example.com', + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT', + }, +}; +``` + +## Default Route Registrations + +The plugin provides five default route registrations: + +### 1. Discovery Routes (`DEFAULT_DISCOVERY_ROUTES`) +- **Prefix:** `/api/v1/discovery` +- **Public:** Yes (no auth required) +- **Endpoints:** `GET /discovery` + +### 2. Metadata Routes (`DEFAULT_METADATA_ROUTES`) +- **Prefix:** `/api/v1/meta` +- **Auth Required:** Yes +- **Endpoints:** + - `GET /meta` - List metadata types + - `GET /meta/:type` - List items of a type + - `GET /meta/:type/:name` - Get specific item + - `PUT /meta/:type/:name` - Create/update item +- **Caching:** Enabled (1 hour TTL) + +### 3. Data CRUD Routes (`DEFAULT_DATA_CRUD_ROUTES`) +- **Prefix:** `/api/v1/data` +- **Auth Required:** Yes +- **Endpoints:** + - `GET /data/:object` - Query records + - `GET /data/:object/:id` - Get record by ID + - `POST /data/:object` - Create record + - `PATCH /data/:object/:id` - Update record + - `DELETE /data/:object/:id` - Delete record +- **Permissions:** `data.read`, `data.create`, `data.update`, `data.delete` + +### 4. Batch Routes (`DEFAULT_BATCH_ROUTES`) +- **Prefix:** `/api/v1/data/:object` +- **Auth Required:** Yes +- **Endpoints:** + - `POST /batch` - Generic batch operation + - `POST /createMany` - Batch create + - `POST /updateMany` - Batch update + - `POST /deleteMany` - Batch delete +- **Timeout:** 60 seconds +- **Permissions:** `data.batch` + operation-specific permissions + +### 5. Permission Routes (`DEFAULT_PERMISSION_ROUTES`) +- **Prefix:** `/api/v1/auth` +- **Auth Required:** Yes +- **Endpoints:** + - `POST /auth/check` - Check permission + - `GET /auth/permissions/:object` - Get object permissions + - `GET /auth/permissions/effective` - Get effective permissions +- **Caching:** Enabled for GET endpoints (5 minutes TTL) + +## Plugin Configuration + +Complete plugin configuration example: + +```typescript +const config: RestApiPluginConfig = { + enabled: true, + basePath: '/api', + version: 'v1', + + // Route registrations + routes: [ + DEFAULT_DISCOVERY_ROUTES, + DEFAULT_METADATA_ROUTES, + DEFAULT_DATA_CRUD_ROUTES, + DEFAULT_BATCH_ROUTES, + DEFAULT_PERMISSION_ROUTES, + ], + + // Request validation + validation: { + enabled: true, + mode: 'strict', + validateBody: true, + validateQuery: true, + includeFieldErrors: true, + }, + + // Response envelope + responseEnvelope: { + enabled: true, + includeMetadata: true, + includeTimestamp: true, + includeRequestId: true, + }, + + // Error handling + errorHandling: { + enabled: true, + includeStackTrace: false, + logErrors: true, + includeDocumentation: true, + }, + + // OpenAPI documentation + openApi: { + enabled: true, + title: 'ObjectStack API', + generateSchemas: true, + includeExamples: true, + }, + + // Global middleware + globalMiddleware: [ + { name: 'cors', type: 'custom', enabled: true, order: 1 }, + { name: 'logger', type: 'logging', enabled: true, order: 5 }, + ], + + // CORS configuration + cors: { + enabled: true, + origins: ['http://localhost:3000'], + credentials: true, + }, + + // Performance settings + performance: { + enableCompression: true, + enableETag: true, + enableCaching: true, + defaultCacheTtl: 300, + }, +}; +``` + +## Middleware Execution Order + +Middleware is executed in the following order (lower numbers first): + +1. **CORS** (order: 1) - Cross-origin resource sharing +2. **Logger** (order: 5) - Request/response logging +3. **Authentication** (order: 10) - JWT/session validation +4. **Validation** (order: 20) - Request schema validation +5. **Response Envelope** (order: 100) - Response wrapping +6. **Error Handler** (order: 200) - Error formatting + +## Usage in Plugin Manifest + +```typescript +{ + "name": "rest_api", + "version": "1.0.0", + "type": "server", + "contributes": { + "routes": [ + { + "prefix": "/api/v1/discovery", + "service": "metadata", + "methods": ["getDiscovery"], + }, + { + "prefix": "/api/v1/meta", + "service": "metadata", + "methods": ["getMetaTypes", "getMetaItems", "getMetaItem", "saveMetaItem"], + }, + { + "prefix": "/api/v1/data", + "service": "data", + "methods": ["findData", "getData", "createData", "updateData", "deleteData"], + }, + ], + }, +} +``` + +## Integration with HttpDispatcher + +The REST API plugin integrates with the HttpDispatcher to route requests to the appropriate service: + +``` +┌─────────────────────────────────────────────────────┐ +│ HTTP Request │ +└─────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Global Middleware │ +│ (CORS, Logging, Authentication) │ +└─────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ HttpDispatcher │ +│ • Match URL prefix │ +│ • Route to service │ +└─────────────────────┬───────────────────────────────┘ + │ + ┌───────────┼───────────┐ + ▼ ▼ ▼ + ┌────────┐ ┌────────┐ ┌────────┐ + │metadata│ │ data │ │ auth │ + │service │ │service │ │service │ + └────┬───┘ └────┬───┘ └────┬───┘ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────────────────────────────────────┐ +│ Route-Specific Middleware │ +│ (Validation, Transformation) │ +└─────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Handler Execution │ +└─────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ Response Envelope & Error Handling │ +└─────────────────────┬───────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────┐ +│ HTTP Response │ +└─────────────────────────────────────────────────────┘ +``` + +## Testing + +The implementation includes comprehensive tests: + +- **33 test cases** covering all schemas and configurations +- Schema validation tests +- Default route registration tests +- Middleware ordering tests +- Schema consistency checks + +Run tests: +```bash +pnpm test plugin-rest-api.test.ts +``` + +## JSON Schema Generation + +The plugin generates 9 JSON schemas: + +1. `ErrorHandlingConfig.json` +2. `OpenApiGenerationConfig.json` +3. `RequestValidationConfig.json` +4. `ResponseEnvelopeConfig.json` +5. `RestApiEndpoint.json` +6. `RestApiPluginConfig.json` +7. `RestApiRouteCategory.json` +8. `RestApiRouteRegistration.json` +9. `ValidationMode.json` + +These schemas can be used for IDE autocomplete, documentation, and validation. + +## Architecture Alignment + +This implementation aligns with industry best practices: + +- **Salesforce REST API**: Metadata and data CRUD patterns +- **Microsoft Dynamics Web API**: Entity operations and OData support +- **Strapi**: Auto-generated REST endpoints from schemas +- **AWS API Gateway**: Route configuration and middleware chains +- **Kubernetes API**: Resource-based routing and discovery + +## Future Enhancements + +Phase 3 and Phase 4 will add additional plugins: + +- **Phase 3**: UI API, Workflow, Analytics, Automation, i18n plugins +- **Phase 4**: Notification, Realtime, AI, Hub, GraphQL plugins + +Each plugin will follow the same pattern established here. diff --git a/packages/spec/json-schema/api/ErrorHandlingConfig.json b/packages/spec/json-schema/api/ErrorHandlingConfig.json new file mode 100644 index 000000000..28ee28774 --- /dev/null +++ b/packages/spec/json-schema/api/ErrorHandlingConfig.json @@ -0,0 +1,66 @@ +{ + "$ref": "#/definitions/ErrorHandlingConfig", + "definitions": { + "ErrorHandlingConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable standardized error handling" + }, + "includeStackTrace": { + "type": "boolean", + "default": false, + "description": "Include stack traces in error responses" + }, + "logErrors": { + "type": "boolean", + "default": true, + "description": "Log errors to system logger" + }, + "exposeInternalErrors": { + "type": "boolean", + "default": false, + "description": "Expose internal error details in responses" + }, + "includeRequestId": { + "type": "boolean", + "default": true, + "description": "Include requestId in error responses" + }, + "includeTimestamp": { + "type": "boolean", + "default": true, + "description": "Include timestamp in error responses" + }, + "includeDocumentation": { + "type": "boolean", + "default": true, + "description": "Include documentation URLs for errors" + }, + "documentationBaseUrl": { + "type": "string", + "format": "uri", + "description": "Base URL for error documentation" + }, + "customErrorMessages": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom error messages by error code" + }, + "redactFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field names to redact from error details" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/OpenApiGenerationConfig.json b/packages/spec/json-schema/api/OpenApiGenerationConfig.json new file mode 100644 index 000000000..6a5fd1b95 --- /dev/null +++ b/packages/spec/json-schema/api/OpenApiGenerationConfig.json @@ -0,0 +1,165 @@ +{ + "$ref": "#/definitions/OpenApiGenerationConfig", + "definitions": { + "OpenApiGenerationConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic OpenAPI documentation generation" + }, + "version": { + "type": "string", + "enum": [ + "3.0.0", + "3.0.1", + "3.0.2", + "3.0.3", + "3.1.0" + ], + "default": "3.0.3", + "description": "OpenAPI specification version" + }, + "title": { + "type": "string", + "default": "ObjectStack API", + "description": "API title" + }, + "description": { + "type": "string", + "description": "API description" + }, + "apiVersion": { + "type": "string", + "default": "1.0.0", + "description": "API version" + }, + "outputPath": { + "type": "string", + "default": "/api/docs/openapi.json", + "description": "URL path to serve OpenAPI JSON" + }, + "uiPath": { + "type": "string", + "default": "/api/docs", + "description": "URL path to serve documentation UI" + }, + "uiFramework": { + "type": "string", + "enum": [ + "swagger-ui", + "redoc", + "rapidoc", + "elements" + ], + "default": "swagger-ui", + "description": "Documentation UI framework" + }, + "includeInternal": { + "type": "boolean", + "default": false, + "description": "Include internal endpoints in documentation" + }, + "generateSchemas": { + "type": "boolean", + "default": true, + "description": "Auto-generate schemas from Zod definitions" + }, + "includeExamples": { + "type": "boolean", + "default": true, + "description": "Include request/response examples" + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Server URL" + }, + "description": { + "type": "string", + "description": "Server description" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "description": "Server URLs for API" + }, + "contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false, + "description": "API contact information" + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "License name" + }, + "url": { + "type": "string", + "format": "uri", + "description": "License URL" + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "API license information" + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey", + "http", + "oauth2", + "openIdConnect" + ] + }, + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Security scheme definitions" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/RequestValidationConfig.json b/packages/spec/json-schema/api/RequestValidationConfig.json new file mode 100644 index 000000000..16002a011 --- /dev/null +++ b/packages/spec/json-schema/api/RequestValidationConfig.json @@ -0,0 +1,60 @@ +{ + "$ref": "#/definitions/RequestValidationConfig", + "definitions": { + "RequestValidationConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic request validation" + }, + "mode": { + "type": "string", + "enum": [ + "strict", + "permissive", + "strip" + ], + "default": "strict", + "description": "How to handle validation errors" + }, + "validateBody": { + "type": "boolean", + "default": true, + "description": "Validate request body against schema" + }, + "validateQuery": { + "type": "boolean", + "default": true, + "description": "Validate query string parameters" + }, + "validateParams": { + "type": "boolean", + "default": true, + "description": "Validate URL path parameters" + }, + "validateHeaders": { + "type": "boolean", + "default": false, + "description": "Validate request headers" + }, + "includeFieldErrors": { + "type": "boolean", + "default": true, + "description": "Include field-level error details in response" + }, + "errorPrefix": { + "type": "string", + "description": "Custom prefix for validation error messages" + }, + "schemaRegistry": { + "type": "string", + "description": "Schema registry name to use for validation" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/ResponseEnvelopeConfig.json b/packages/spec/json-schema/api/ResponseEnvelopeConfig.json new file mode 100644 index 000000000..9a28985fc --- /dev/null +++ b/packages/spec/json-schema/api/ResponseEnvelopeConfig.json @@ -0,0 +1,52 @@ +{ + "$ref": "#/definitions/ResponseEnvelopeConfig", + "definitions": { + "ResponseEnvelopeConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic response envelope wrapping" + }, + "includeMetadata": { + "type": "boolean", + "default": true, + "description": "Include meta object in responses" + }, + "includeTimestamp": { + "type": "boolean", + "default": true, + "description": "Include timestamp in response metadata" + }, + "includeRequestId": { + "type": "boolean", + "default": true, + "description": "Include requestId in response metadata" + }, + "includeDuration": { + "type": "boolean", + "default": false, + "description": "Include request duration in ms" + }, + "includeTraceId": { + "type": "boolean", + "default": false, + "description": "Include distributed traceId" + }, + "customMetadata": { + "type": "object", + "additionalProperties": {}, + "description": "Additional metadata fields to include" + }, + "skipIfWrapped": { + "type": "boolean", + "default": true, + "description": "Skip wrapping if response already has success field" + } + }, + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/RestApiEndpoint.json b/packages/spec/json-schema/api/RestApiEndpoint.json new file mode 100644 index 000000000..77ce17fe3 --- /dev/null +++ b/packages/spec/json-schema/api/RestApiEndpoint.json @@ -0,0 +1,111 @@ +{ + "$ref": "#/definitions/RestApiEndpoint", + "definitions": { + "RestApiEndpoint": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ], + "description": "HTTP method for this endpoint" + }, + "path": { + "type": "string", + "description": "URL path pattern (e.g., /api/v1/data/:object/:id)" + }, + "handler": { + "type": "string", + "description": "Protocol method name or handler identifier" + }, + "category": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ], + "description": "Route category" + }, + "public": { + "type": "boolean", + "default": false, + "description": "Is publicly accessible without authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions (e.g., [\"data.read\", \"object.account.read\"])" + }, + "summary": { + "type": "string", + "description": "Short description for OpenAPI" + }, + "description": { + "type": "string", + "description": "Detailed description for OpenAPI" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OpenAPI tags for grouping" + }, + "requestSchema": { + "type": "string", + "description": "Request schema name (for validation)" + }, + "responseSchema": { + "type": "string", + "description": "Response schema name (for documentation)" + }, + "timeout": { + "type": "integer", + "description": "Request timeout in milliseconds" + }, + "rateLimit": { + "type": "string", + "description": "Rate limit policy name" + }, + "cacheable": { + "type": "boolean", + "default": false, + "description": "Whether response can be cached" + }, + "cacheTtl": { + "type": "integer", + "description": "Cache TTL in seconds" + } + }, + "required": [ + "method", + "path", + "handler", + "category" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/RestApiPluginConfig.json b/packages/spec/json-schema/api/RestApiPluginConfig.json new file mode 100644 index 000000000..e6e9000f0 --- /dev/null +++ b/packages/spec/json-schema/api/RestApiPluginConfig.json @@ -0,0 +1,739 @@ +{ + "$ref": "#/definitions/RestApiPluginConfig", + "definitions": { + "RestApiPluginConfig": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable REST API plugin" + }, + "basePath": { + "type": "string", + "default": "/api", + "description": "Base path for all API routes" + }, + "version": { + "type": "string", + "default": "v1", + "description": "API version identifier" + }, + "routes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "pattern": "^\\/", + "description": "URL path prefix for this route group" + }, + "service": { + "type": "string", + "description": "Core service name (metadata, data, auth, etc.)" + }, + "category": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ], + "description": "Primary category for this route group" + }, + "methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Protocol method names implemented" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ], + "description": "HTTP method for this endpoint" + }, + "path": { + "type": "string", + "description": "URL path pattern (e.g., /api/v1/data/:object/:id)" + }, + "handler": { + "type": "string", + "description": "Protocol method name or handler identifier" + }, + "category": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ], + "description": "Route category" + }, + "public": { + "type": "boolean", + "default": false, + "description": "Is publicly accessible without authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions (e.g., [\"data.read\", \"object.account.read\"])" + }, + "summary": { + "type": "string", + "description": "Short description for OpenAPI" + }, + "description": { + "type": "string", + "description": "Detailed description for OpenAPI" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OpenAPI tags for grouping" + }, + "requestSchema": { + "type": "string", + "description": "Request schema name (for validation)" + }, + "responseSchema": { + "type": "string", + "description": "Response schema name (for documentation)" + }, + "timeout": { + "type": "integer", + "description": "Request timeout in milliseconds" + }, + "rateLimit": { + "type": "string", + "description": "Rate limit policy name" + }, + "cacheable": { + "type": "boolean", + "default": false, + "description": "Whether response can be cached" + }, + "cacheTtl": { + "type": "integer", + "description": "Cache TTL in seconds" + } + }, + "required": [ + "method", + "path", + "handler", + "category" + ], + "additionalProperties": false + }, + "description": "Endpoint definitions" + }, + "middleware": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Middleware name (snake_case)" + }, + "type": { + "type": "string", + "enum": [ + "authentication", + "authorization", + "logging", + "validation", + "transformation", + "error", + "custom" + ], + "description": "Middleware type" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether middleware is enabled" + }, + "order": { + "type": "integer", + "default": 100, + "description": "Execution order priority" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Middleware configuration object" + }, + "paths": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include path patterns (glob)" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exclude path patterns (glob)" + } + }, + "additionalProperties": false, + "description": "Path filtering" + } + }, + "required": [ + "name", + "type" + ], + "additionalProperties": false + }, + "description": "Middleware stack for this route group" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Whether authentication is required by default" + }, + "documentation": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Route group title" + }, + "description": { + "type": "string", + "description": "Route group description" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OpenAPI tags" + } + }, + "additionalProperties": false, + "description": "Documentation metadata for this route group" + } + }, + "required": [ + "prefix", + "service", + "category" + ], + "additionalProperties": false + }, + "description": "Route registrations" + }, + "validation": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic request validation" + }, + "mode": { + "type": "string", + "enum": [ + "strict", + "permissive", + "strip" + ], + "default": "strict", + "description": "How to handle validation errors" + }, + "validateBody": { + "type": "boolean", + "default": true, + "description": "Validate request body against schema" + }, + "validateQuery": { + "type": "boolean", + "default": true, + "description": "Validate query string parameters" + }, + "validateParams": { + "type": "boolean", + "default": true, + "description": "Validate URL path parameters" + }, + "validateHeaders": { + "type": "boolean", + "default": false, + "description": "Validate request headers" + }, + "includeFieldErrors": { + "type": "boolean", + "default": true, + "description": "Include field-level error details in response" + }, + "errorPrefix": { + "type": "string", + "description": "Custom prefix for validation error messages" + }, + "schemaRegistry": { + "type": "string", + "description": "Schema registry name to use for validation" + } + }, + "additionalProperties": false, + "description": "Request validation configuration" + }, + "responseEnvelope": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic response envelope wrapping" + }, + "includeMetadata": { + "type": "boolean", + "default": true, + "description": "Include meta object in responses" + }, + "includeTimestamp": { + "type": "boolean", + "default": true, + "description": "Include timestamp in response metadata" + }, + "includeRequestId": { + "type": "boolean", + "default": true, + "description": "Include requestId in response metadata" + }, + "includeDuration": { + "type": "boolean", + "default": false, + "description": "Include request duration in ms" + }, + "includeTraceId": { + "type": "boolean", + "default": false, + "description": "Include distributed traceId" + }, + "customMetadata": { + "type": "object", + "additionalProperties": {}, + "description": "Additional metadata fields to include" + }, + "skipIfWrapped": { + "type": "boolean", + "default": true, + "description": "Skip wrapping if response already has success field" + } + }, + "additionalProperties": false, + "description": "Response envelope configuration" + }, + "errorHandling": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable standardized error handling" + }, + "includeStackTrace": { + "type": "boolean", + "default": false, + "description": "Include stack traces in error responses" + }, + "logErrors": { + "type": "boolean", + "default": true, + "description": "Log errors to system logger" + }, + "exposeInternalErrors": { + "type": "boolean", + "default": false, + "description": "Expose internal error details in responses" + }, + "includeRequestId": { + "type": "boolean", + "default": true, + "description": "Include requestId in error responses" + }, + "includeTimestamp": { + "type": "boolean", + "default": true, + "description": "Include timestamp in error responses" + }, + "includeDocumentation": { + "type": "boolean", + "default": true, + "description": "Include documentation URLs for errors" + }, + "documentationBaseUrl": { + "type": "string", + "format": "uri", + "description": "Base URL for error documentation" + }, + "customErrorMessages": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Custom error messages by error code" + }, + "redactFields": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Field names to redact from error details" + } + }, + "additionalProperties": false, + "description": "Error handling configuration" + }, + "openApi": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic OpenAPI documentation generation" + }, + "version": { + "type": "string", + "enum": [ + "3.0.0", + "3.0.1", + "3.0.2", + "3.0.3", + "3.1.0" + ], + "default": "3.0.3", + "description": "OpenAPI specification version" + }, + "title": { + "type": "string", + "default": "ObjectStack API", + "description": "API title" + }, + "description": { + "type": "string", + "description": "API description" + }, + "apiVersion": { + "type": "string", + "default": "1.0.0", + "description": "API version" + }, + "outputPath": { + "type": "string", + "default": "/api/docs/openapi.json", + "description": "URL path to serve OpenAPI JSON" + }, + "uiPath": { + "type": "string", + "default": "/api/docs", + "description": "URL path to serve documentation UI" + }, + "uiFramework": { + "type": "string", + "enum": [ + "swagger-ui", + "redoc", + "rapidoc", + "elements" + ], + "default": "swagger-ui", + "description": "Documentation UI framework" + }, + "includeInternal": { + "type": "boolean", + "default": false, + "description": "Include internal endpoints in documentation" + }, + "generateSchemas": { + "type": "boolean", + "default": true, + "description": "Auto-generate schemas from Zod definitions" + }, + "includeExamples": { + "type": "boolean", + "default": true, + "description": "Include request/response examples" + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "Server URL" + }, + "description": { + "type": "string", + "description": "Server description" + } + }, + "required": [ + "url" + ], + "additionalProperties": false + }, + "description": "Server URLs for API" + }, + "contact": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "email": { + "type": "string", + "format": "email" + } + }, + "additionalProperties": false, + "description": "API contact information" + }, + "license": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "License name" + }, + "url": { + "type": "string", + "format": "uri", + "description": "License URL" + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "description": "API license information" + }, + "securitySchemes": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": [ + "apiKey", + "http", + "oauth2", + "openIdConnect" + ] + }, + "scheme": { + "type": "string" + }, + "bearerFormat": { + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + }, + "description": "Security scheme definitions" + } + }, + "additionalProperties": false, + "description": "OpenAPI documentation configuration" + }, + "globalMiddleware": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Middleware name (snake_case)" + }, + "type": { + "type": "string", + "enum": [ + "authentication", + "authorization", + "logging", + "validation", + "transformation", + "error", + "custom" + ], + "description": "Middleware type" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether middleware is enabled" + }, + "order": { + "type": "integer", + "default": 100, + "description": "Execution order priority" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Middleware configuration object" + }, + "paths": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include path patterns (glob)" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exclude path patterns (glob)" + } + }, + "additionalProperties": false, + "description": "Path filtering" + } + }, + "required": [ + "name", + "type" + ], + "additionalProperties": false + }, + "description": "Global middleware stack" + }, + "cors": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "origins": { + "type": "array", + "items": { + "type": "string" + } + }, + "methods": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ] + } + }, + "credentials": { + "type": "boolean", + "default": true + } + }, + "additionalProperties": false, + "description": "CORS configuration" + }, + "performance": { + "type": "object", + "properties": { + "enableCompression": { + "type": "boolean", + "default": true, + "description": "Enable response compression" + }, + "enableETag": { + "type": "boolean", + "default": true, + "description": "Enable ETag generation" + }, + "enableCaching": { + "type": "boolean", + "default": true, + "description": "Enable HTTP caching" + }, + "defaultCacheTtl": { + "type": "integer", + "default": 300, + "description": "Default cache TTL in seconds" + } + }, + "additionalProperties": false, + "description": "Performance optimization settings" + } + }, + "required": [ + "routes" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/RestApiRouteCategory.json b/packages/spec/json-schema/api/RestApiRouteCategory.json new file mode 100644 index 000000000..4d9ab4a06 --- /dev/null +++ b/packages/spec/json-schema/api/RestApiRouteCategory.json @@ -0,0 +1,25 @@ +{ + "$ref": "#/definitions/RestApiRouteCategory", + "definitions": { + "RestApiRouteCategory": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/RestApiRouteRegistration.json b/packages/spec/json-schema/api/RestApiRouteRegistration.json new file mode 100644 index 000000000..5d6148e62 --- /dev/null +++ b/packages/spec/json-schema/api/RestApiRouteRegistration.json @@ -0,0 +1,257 @@ +{ + "$ref": "#/definitions/RestApiRouteRegistration", + "definitions": { + "RestApiRouteRegistration": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "pattern": "^\\/", + "description": "URL path prefix for this route group" + }, + "service": { + "type": "string", + "description": "Core service name (metadata, data, auth, etc.)" + }, + "category": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ], + "description": "Primary category for this route group" + }, + "methods": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Protocol method names implemented" + }, + "endpoints": { + "type": "array", + "items": { + "type": "object", + "properties": { + "method": { + "type": "string", + "enum": [ + "GET", + "POST", + "PUT", + "DELETE", + "PATCH", + "HEAD", + "OPTIONS" + ], + "description": "HTTP method for this endpoint" + }, + "path": { + "type": "string", + "description": "URL path pattern (e.g., /api/v1/data/:object/:id)" + }, + "handler": { + "type": "string", + "description": "Protocol method name or handler identifier" + }, + "category": { + "type": "string", + "enum": [ + "discovery", + "metadata", + "data", + "batch", + "permission", + "analytics", + "automation", + "workflow", + "ui", + "realtime", + "notification", + "ai", + "i18n", + "hub" + ], + "description": "Route category" + }, + "public": { + "type": "boolean", + "default": false, + "description": "Is publicly accessible without authentication" + }, + "permissions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Required permissions (e.g., [\"data.read\", \"object.account.read\"])" + }, + "summary": { + "type": "string", + "description": "Short description for OpenAPI" + }, + "description": { + "type": "string", + "description": "Detailed description for OpenAPI" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OpenAPI tags for grouping" + }, + "requestSchema": { + "type": "string", + "description": "Request schema name (for validation)" + }, + "responseSchema": { + "type": "string", + "description": "Response schema name (for documentation)" + }, + "timeout": { + "type": "integer", + "description": "Request timeout in milliseconds" + }, + "rateLimit": { + "type": "string", + "description": "Rate limit policy name" + }, + "cacheable": { + "type": "boolean", + "default": false, + "description": "Whether response can be cached" + }, + "cacheTtl": { + "type": "integer", + "description": "Cache TTL in seconds" + } + }, + "required": [ + "method", + "path", + "handler", + "category" + ], + "additionalProperties": false + }, + "description": "Endpoint definitions" + }, + "middleware": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$", + "description": "Middleware name (snake_case)" + }, + "type": { + "type": "string", + "enum": [ + "authentication", + "authorization", + "logging", + "validation", + "transformation", + "error", + "custom" + ], + "description": "Middleware type" + }, + "enabled": { + "type": "boolean", + "default": true, + "description": "Whether middleware is enabled" + }, + "order": { + "type": "integer", + "default": 100, + "description": "Execution order priority" + }, + "config": { + "type": "object", + "additionalProperties": {}, + "description": "Middleware configuration object" + }, + "paths": { + "type": "object", + "properties": { + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include path patterns (glob)" + }, + "exclude": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exclude path patterns (glob)" + } + }, + "additionalProperties": false, + "description": "Path filtering" + } + }, + "required": [ + "name", + "type" + ], + "additionalProperties": false + }, + "description": "Middleware stack for this route group" + }, + "authRequired": { + "type": "boolean", + "default": true, + "description": "Whether authentication is required by default" + }, + "documentation": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Route group title" + }, + "description": { + "type": "string", + "description": "Route group description" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "OpenAPI tags" + } + }, + "additionalProperties": false, + "description": "Documentation metadata for this route group" + } + }, + "required": [ + "prefix", + "service", + "category" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/json-schema/api/ValidationMode.json b/packages/spec/json-schema/api/ValidationMode.json new file mode 100644 index 000000000..33ef07495 --- /dev/null +++ b/packages/spec/json-schema/api/ValidationMode.json @@ -0,0 +1,14 @@ +{ + "$ref": "#/definitions/ValidationMode", + "definitions": { + "ValidationMode": { + "type": "string", + "enum": [ + "strict", + "permissive", + "strip" + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" +} \ No newline at end of file diff --git a/packages/spec/src/api/index.ts b/packages/spec/src/api/index.ts index 95c8409ca..cb7bf2653 100644 --- a/packages/spec/src/api/index.ts +++ b/packages/spec/src/api/index.ts @@ -8,6 +8,8 @@ * - Batch operations * - Metadata caching * - Hub Management APIs + * - HttpDispatcher routing + * - API versioning */ export * from './contract.zod'; @@ -27,6 +29,7 @@ export * from './hub.zod'; export * from './registry.zod'; export * from './documentation.zod'; export * from './analytics.zod'; +export * from './versioning.zod'; // Legacy interface export (deprecated) // export type { IObjectStackProtocol } from './protocol'; @@ -35,3 +38,4 @@ export * from './auth.zod'; export * from './storage.zod'; export * from './metadata.zod'; export * from './dispatcher.zod'; +export * from './plugin-rest-api.zod'; diff --git a/packages/spec/src/api/plugin-rest-api.test.ts b/packages/spec/src/api/plugin-rest-api.test.ts new file mode 100644 index 000000000..c1d0d4f0a --- /dev/null +++ b/packages/spec/src/api/plugin-rest-api.test.ts @@ -0,0 +1,725 @@ +import { describe, it, expect } from 'vitest'; +import { + RestApiRouteCategory, + RestApiEndpointSchema, + RestApiRouteRegistrationSchema, + RequestValidationConfigSchema, + ResponseEnvelopeConfigSchema, + ErrorHandlingConfigSchema, + OpenApiGenerationConfigSchema, + RestApiPluginConfigSchema, + ValidationMode, + DEFAULT_DISCOVERY_ROUTES, + DEFAULT_METADATA_ROUTES, + DEFAULT_DATA_CRUD_ROUTES, + DEFAULT_BATCH_ROUTES, + DEFAULT_PERMISSION_ROUTES, + DEFAULT_VIEW_ROUTES, + DEFAULT_WORKFLOW_ROUTES, + DEFAULT_REALTIME_ROUTES, + DEFAULT_NOTIFICATION_ROUTES, + DEFAULT_AI_ROUTES, + DEFAULT_I18N_ROUTES, + DEFAULT_ANALYTICS_ROUTES, + DEFAULT_HUB_ROUTES, + DEFAULT_AUTOMATION_ROUTES, + getDefaultRouteRegistrations, +} from './plugin-rest-api.zod'; + +describe('plugin-rest-api.zod', () => { + describe('RestApiRouteCategory', () => { + it('should validate valid route categories', () => { + expect(RestApiRouteCategory.parse('discovery')).toBe('discovery'); + expect(RestApiRouteCategory.parse('metadata')).toBe('metadata'); + expect(RestApiRouteCategory.parse('data')).toBe('data'); + expect(RestApiRouteCategory.parse('batch')).toBe('batch'); + expect(RestApiRouteCategory.parse('permission')).toBe('permission'); + }); + + it('should reject invalid route categories', () => { + expect(() => RestApiRouteCategory.parse('invalid')).toThrow(); + }); + }); + + describe('RestApiEndpointSchema', () => { + it('should validate a basic endpoint', () => { + const endpoint = RestApiEndpointSchema.parse({ + method: 'GET', + path: '/api/v1/discovery', + handler: 'getDiscovery', + category: 'discovery', + }); + + expect(endpoint.method).toBe('GET'); + expect(endpoint.path).toBe('/api/v1/discovery'); + expect(endpoint.handler).toBe('getDiscovery'); + expect(endpoint.category).toBe('discovery'); + expect(endpoint.public).toBe(false); // default + }); + + it('should validate endpoint with all fields', () => { + const endpoint = RestApiEndpointSchema.parse({ + method: 'POST', + path: '/api/v1/data/:object', + handler: 'createData', + category: 'data', + public: false, + permissions: ['data.create'], + summary: 'Create a record', + description: 'Creates a new record in the specified object', + tags: ['Data', 'CRUD'], + requestSchema: 'CreateRequestSchema', + responseSchema: 'SingleRecordResponseSchema', + timeout: 30000, + rateLimit: 'standard', + cacheable: false, + }); + + expect(endpoint.permissions).toEqual(['data.create']); + expect(endpoint.summary).toBe('Create a record'); + expect(endpoint.tags).toEqual(['Data', 'CRUD']); + expect(endpoint.timeout).toBe(30000); + }); + + it('should default public to false', () => { + const endpoint = RestApiEndpointSchema.parse({ + method: 'GET', + path: '/test', + handler: 'test', + category: 'data', + }); + + expect(endpoint.public).toBe(false); + }); + + it('should default cacheable to false', () => { + const endpoint = RestApiEndpointSchema.parse({ + method: 'GET', + path: '/test', + handler: 'test', + category: 'metadata', + }); + + expect(endpoint.cacheable).toBe(false); + }); + }); + + describe('RestApiRouteRegistrationSchema', () => { + it('should validate a basic route registration', () => { + const registration = RestApiRouteRegistrationSchema.parse({ + prefix: '/api/v1/data', + service: 'data', + category: 'data', + }); + + expect(registration.prefix).toBe('/api/v1/data'); + expect(registration.service).toBe('data'); + expect(registration.category).toBe('data'); + expect(registration.authRequired).toBe(true); // default + }); + + it('should validate route registration with endpoints', () => { + const registration = RestApiRouteRegistrationSchema.parse({ + prefix: '/api/v1/data', + service: 'data', + category: 'data', + methods: ['findData', 'getData', 'createData'], + endpoints: [ + { + method: 'GET', + path: '/:object', + handler: 'findData', + category: 'data', + summary: 'Find records', + }, + { + method: 'GET', + path: '/:object/:id', + handler: 'getData', + category: 'data', + summary: 'Get record', + }, + ], + authRequired: true, + }); + + expect(registration.methods).toHaveLength(3); + expect(registration.endpoints).toHaveLength(2); + expect(registration.endpoints?.[0].method).toBe('GET'); + }); + + it('should validate route registration with middleware', () => { + const registration = RestApiRouteRegistrationSchema.parse({ + prefix: '/api/v1/meta', + service: 'metadata', + category: 'metadata', + middleware: [ + { + name: 'auth', + type: 'authentication', + enabled: true, + order: 10, + }, + { + name: 'validation', + type: 'validation', + enabled: true, + order: 20, + }, + ], + }); + + expect(registration.middleware).toHaveLength(2); + expect(registration.middleware?.[0].name).toBe('auth'); + expect(registration.middleware?.[0].type).toBe('authentication'); + expect(registration.middleware?.[1].type).toBe('validation'); + }); + + it('should require prefix to start with /', () => { + expect(() => + RestApiRouteRegistrationSchema.parse({ + prefix: 'api/v1/data', // missing leading / + service: 'data', + category: 'data', + }) + ).toThrow(); + }); + }); + + describe('RequestValidationConfigSchema', () => { + it('should validate with defaults', () => { + const config = RequestValidationConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.mode).toBe('strict'); + expect(config.validateBody).toBe(true); + expect(config.validateQuery).toBe(true); + expect(config.validateParams).toBe(true); + expect(config.validateHeaders).toBe(false); + expect(config.includeFieldErrors).toBe(true); + }); + + it('should validate validation modes', () => { + expect(ValidationMode.parse('strict')).toBe('strict'); + expect(ValidationMode.parse('permissive')).toBe('permissive'); + expect(ValidationMode.parse('strip')).toBe('strip'); + expect(() => ValidationMode.parse('invalid')).toThrow(); + }); + + it('should validate custom config', () => { + const config = RequestValidationConfigSchema.parse({ + enabled: true, + mode: 'permissive', + validateBody: true, + validateQuery: false, + validateParams: false, + validateHeaders: true, + includeFieldErrors: false, + errorPrefix: 'Validation Error: ', + }); + + expect(config.mode).toBe('permissive'); + expect(config.validateQuery).toBe(false); + expect(config.validateHeaders).toBe(true); + expect(config.errorPrefix).toBe('Validation Error: '); + }); + }); + + describe('ResponseEnvelopeConfigSchema', () => { + it('should validate with defaults', () => { + const config = ResponseEnvelopeConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.includeMetadata).toBe(true); + expect(config.includeTimestamp).toBe(true); + expect(config.includeRequestId).toBe(true); + expect(config.includeDuration).toBe(false); + expect(config.includeTraceId).toBe(false); + expect(config.skipIfWrapped).toBe(true); + }); + + it('should validate custom config', () => { + const config = ResponseEnvelopeConfigSchema.parse({ + enabled: true, + includeMetadata: true, + includeTimestamp: true, + includeRequestId: true, + includeDuration: true, + includeTraceId: true, + customMetadata: { + version: '1.0.0', + environment: 'production', + }, + skipIfWrapped: false, + }); + + expect(config.includeDuration).toBe(true); + expect(config.includeTraceId).toBe(true); + expect(config.customMetadata).toEqual({ + version: '1.0.0', + environment: 'production', + }); + expect(config.skipIfWrapped).toBe(false); + }); + }); + + describe('ErrorHandlingConfigSchema', () => { + it('should validate with defaults', () => { + const config = ErrorHandlingConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.includeStackTrace).toBe(false); + expect(config.logErrors).toBe(true); + expect(config.exposeInternalErrors).toBe(false); + expect(config.includeRequestId).toBe(true); + expect(config.includeTimestamp).toBe(true); + expect(config.includeDocumentation).toBe(true); + }); + + it('should validate custom config', () => { + const config = ErrorHandlingConfigSchema.parse({ + enabled: true, + includeStackTrace: true, + logErrors: true, + exposeInternalErrors: true, + includeRequestId: true, + includeTimestamp: true, + includeDocumentation: true, + documentationBaseUrl: 'https://docs.example.com/errors', + customErrorMessages: { + validation_error: 'Invalid input data', + not_found: 'Resource not found', + }, + redactFields: ['password', 'ssn', 'creditCard'], + }); + + expect(config.includeStackTrace).toBe(true); + expect(config.documentationBaseUrl).toBe('https://docs.example.com/errors'); + expect(config.customErrorMessages).toHaveProperty('validation_error'); + expect(config.redactFields).toEqual(['password', 'ssn', 'creditCard']); + }); + + it('should validate URL format for documentationBaseUrl', () => { + expect(() => + ErrorHandlingConfigSchema.parse({ + documentationBaseUrl: 'not-a-url', + }) + ).toThrow(); + + expect( + ErrorHandlingConfigSchema.parse({ + documentationBaseUrl: 'https://docs.example.com', + }) + ).toBeTruthy(); + }); + }); + + describe('OpenApiGenerationConfigSchema', () => { + it('should validate with defaults', () => { + const config = OpenApiGenerationConfigSchema.parse({}); + + expect(config.enabled).toBe(true); + expect(config.version).toBe('3.0.3'); + expect(config.title).toBe('ObjectStack API'); + expect(config.apiVersion).toBe('1.0.0'); + expect(config.outputPath).toBe('/api/docs/openapi.json'); + expect(config.uiPath).toBe('/api/docs'); + expect(config.uiFramework).toBe('swagger-ui'); + expect(config.includeInternal).toBe(false); + expect(config.generateSchemas).toBe(true); + expect(config.includeExamples).toBe(true); + }); + + it('should validate OpenAPI versions', () => { + const versions = ['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']; + + versions.forEach(version => { + const config = OpenApiGenerationConfigSchema.parse({ version }); + expect(config.version).toBe(version); + }); + + expect(() => + OpenApiGenerationConfigSchema.parse({ version: '2.0.0' }) + ).toThrow(); + }); + + it('should validate UI frameworks', () => { + const frameworks = ['swagger-ui', 'redoc', 'rapidoc', 'elements']; + + frameworks.forEach(framework => { + const config = OpenApiGenerationConfigSchema.parse({ uiFramework: framework }); + expect(config.uiFramework).toBe(framework); + }); + + expect(() => + OpenApiGenerationConfigSchema.parse({ uiFramework: 'invalid' }) + ).toThrow(); + }); + + it('should validate complete config', () => { + const config = OpenApiGenerationConfigSchema.parse({ + enabled: true, + version: '3.1.0', + title: 'My API', + description: 'API for my application', + apiVersion: '2.0.0', + outputPath: '/docs/api.json', + uiPath: '/docs', + uiFramework: 'redoc', + includeInternal: true, + generateSchemas: true, + includeExamples: true, + servers: [ + { url: 'https://api.example.com', description: 'Production' }, + { url: 'https://api-staging.example.com', description: 'Staging' }, + ], + contact: { + name: 'API Support', + url: 'https://example.com/support', + email: 'api@example.com', + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT', + }, + }); + + expect(config.title).toBe('My API'); + expect(config.servers).toHaveLength(2); + expect(config.contact?.email).toBe('api@example.com'); + expect(config.license?.name).toBe('MIT'); + }); + }); + + describe('RestApiPluginConfigSchema', () => { + it('should validate minimal config', () => { + const config = RestApiPluginConfigSchema.parse({ + routes: [], + }); + + expect(config.enabled).toBe(true); + expect(config.basePath).toBe('/api'); + expect(config.version).toBe('v1'); + expect(config.routes).toEqual([]); + }); + + it('should validate complete config', () => { + const config = RestApiPluginConfigSchema.parse({ + enabled: true, + basePath: '/api', + version: 'v2', + routes: [ + { + prefix: '/api/v2/data', + service: 'data', + category: 'data', + }, + ], + validation: { + enabled: true, + mode: 'strict', + }, + responseEnvelope: { + enabled: true, + includeMetadata: true, + }, + errorHandling: { + enabled: true, + includeStackTrace: false, + }, + openApi: { + enabled: true, + title: 'My API', + }, + globalMiddleware: [ + { + name: 'cors', + type: 'custom', + enabled: true, + order: 1, + }, + ], + cors: { + enabled: true, + origins: ['http://localhost:3000'], + methods: ['GET', 'POST', 'PUT', 'DELETE'], + credentials: true, + }, + performance: { + enableCompression: true, + enableETag: true, + enableCaching: true, + defaultCacheTtl: 600, + }, + }); + + expect(config.version).toBe('v2'); + expect(config.routes).toHaveLength(1); + expect(config.validation?.mode).toBe('strict'); + expect(config.openApi?.title).toBe('My API'); + expect(config.cors?.origins).toContain('http://localhost:3000'); + expect(config.performance?.defaultCacheTtl).toBe(600); + }); + }); + + describe('Default Route Registrations', () => { + it('should validate DEFAULT_DISCOVERY_ROUTES', () => { + expect(DEFAULT_DISCOVERY_ROUTES.prefix).toBe('/api/v1/discovery'); + expect(DEFAULT_DISCOVERY_ROUTES.service).toBe('metadata'); + expect(DEFAULT_DISCOVERY_ROUTES.category).toBe('discovery'); + expect(DEFAULT_DISCOVERY_ROUTES.authRequired).toBe(false); + expect(DEFAULT_DISCOVERY_ROUTES.endpoints).toHaveLength(1); + expect(DEFAULT_DISCOVERY_ROUTES.endpoints?.[0].public).toBe(true); + }); + + it('should validate DEFAULT_METADATA_ROUTES', () => { + expect(DEFAULT_METADATA_ROUTES.prefix).toBe('/api/v1/meta'); + expect(DEFAULT_METADATA_ROUTES.service).toBe('metadata'); + expect(DEFAULT_METADATA_ROUTES.category).toBe('metadata'); + expect(DEFAULT_METADATA_ROUTES.authRequired).toBe(true); + expect(DEFAULT_METADATA_ROUTES.endpoints).toHaveLength(4); + expect(DEFAULT_METADATA_ROUTES.middleware).toBeDefined(); + }); + + it('should validate DEFAULT_DATA_CRUD_ROUTES', () => { + expect(DEFAULT_DATA_CRUD_ROUTES.prefix).toBe('/api/v1/data'); + expect(DEFAULT_DATA_CRUD_ROUTES.service).toBe('data'); + expect(DEFAULT_DATA_CRUD_ROUTES.category).toBe('data'); + expect(DEFAULT_DATA_CRUD_ROUTES.methods).toContain('findData'); + expect(DEFAULT_DATA_CRUD_ROUTES.methods).toContain('getData'); + expect(DEFAULT_DATA_CRUD_ROUTES.methods).toContain('createData'); + expect(DEFAULT_DATA_CRUD_ROUTES.methods).toContain('updateData'); + expect(DEFAULT_DATA_CRUD_ROUTES.methods).toContain('deleteData'); + expect(DEFAULT_DATA_CRUD_ROUTES.endpoints).toHaveLength(5); + }); + + it('should validate DEFAULT_BATCH_ROUTES', () => { + expect(DEFAULT_BATCH_ROUTES.prefix).toBe('/api/v1/data/:object'); + expect(DEFAULT_BATCH_ROUTES.service).toBe('data'); + expect(DEFAULT_BATCH_ROUTES.category).toBe('batch'); + expect(DEFAULT_BATCH_ROUTES.methods).toContain('batchData'); + expect(DEFAULT_BATCH_ROUTES.methods).toContain('createManyData'); + expect(DEFAULT_BATCH_ROUTES.methods).toContain('updateManyData'); + expect(DEFAULT_BATCH_ROUTES.methods).toContain('deleteManyData'); + expect(DEFAULT_BATCH_ROUTES.endpoints).toHaveLength(4); + + // Verify batch endpoints have longer timeouts + DEFAULT_BATCH_ROUTES.endpoints?.forEach(endpoint => { + expect(endpoint.timeout).toBe(60000); + }); + }); + + it('should validate DEFAULT_PERMISSION_ROUTES', () => { + expect(DEFAULT_PERMISSION_ROUTES.prefix).toBe('/api/v1/auth'); + expect(DEFAULT_PERMISSION_ROUTES.service).toBe('auth'); + expect(DEFAULT_PERMISSION_ROUTES.category).toBe('permission'); + expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('checkPermission'); + expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('getObjectPermissions'); + expect(DEFAULT_PERMISSION_ROUTES.methods).toContain('getEffectivePermissions'); + expect(DEFAULT_PERMISSION_ROUTES.endpoints).toHaveLength(3); + }); + + it('should validate DEFAULT_VIEW_ROUTES', () => { + expect(DEFAULT_VIEW_ROUTES.prefix).toBe('/api/v1/ui'); + expect(DEFAULT_VIEW_ROUTES.service).toBe('ui'); + expect(DEFAULT_VIEW_ROUTES.category).toBe('ui'); + expect(DEFAULT_VIEW_ROUTES.methods).toContain('listViews'); + expect(DEFAULT_VIEW_ROUTES.methods).toContain('getView'); + expect(DEFAULT_VIEW_ROUTES.methods).toContain('createView'); + expect(DEFAULT_VIEW_ROUTES.methods).toContain('updateView'); + expect(DEFAULT_VIEW_ROUTES.methods).toContain('deleteView'); + expect(DEFAULT_VIEW_ROUTES.endpoints).toHaveLength(5); + }); + + it('should validate DEFAULT_WORKFLOW_ROUTES', () => { + expect(DEFAULT_WORKFLOW_ROUTES.prefix).toBe('/api/v1/workflow'); + expect(DEFAULT_WORKFLOW_ROUTES.service).toBe('workflow'); + expect(DEFAULT_WORKFLOW_ROUTES.category).toBe('workflow'); + expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('getWorkflowConfig'); + expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('getWorkflowState'); + expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('workflowTransition'); + expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('workflowApprove'); + expect(DEFAULT_WORKFLOW_ROUTES.methods).toContain('workflowReject'); + expect(DEFAULT_WORKFLOW_ROUTES.endpoints).toHaveLength(5); + }); + + it('should validate DEFAULT_REALTIME_ROUTES', () => { + expect(DEFAULT_REALTIME_ROUTES.prefix).toBe('/api/v1/realtime'); + expect(DEFAULT_REALTIME_ROUTES.service).toBe('realtime'); + expect(DEFAULT_REALTIME_ROUTES.category).toBe('realtime'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeConnect'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeDisconnect'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeSubscribe'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('realtimeUnsubscribe'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('setPresence'); + expect(DEFAULT_REALTIME_ROUTES.methods).toContain('getPresence'); + expect(DEFAULT_REALTIME_ROUTES.endpoints).toHaveLength(6); + }); + + it('should validate DEFAULT_NOTIFICATION_ROUTES', () => { + expect(DEFAULT_NOTIFICATION_ROUTES.prefix).toBe('/api/v1/notifications'); + expect(DEFAULT_NOTIFICATION_ROUTES.service).toBe('notification'); + expect(DEFAULT_NOTIFICATION_ROUTES.category).toBe('notification'); + expect(DEFAULT_NOTIFICATION_ROUTES.methods).toContain('registerDevice'); + expect(DEFAULT_NOTIFICATION_ROUTES.methods).toContain('listNotifications'); + expect(DEFAULT_NOTIFICATION_ROUTES.methods).toContain('markNotificationsRead'); + expect(DEFAULT_NOTIFICATION_ROUTES.methods).toContain('markAllNotificationsRead'); + expect(DEFAULT_NOTIFICATION_ROUTES.endpoints).toHaveLength(7); + }); + + it('should validate DEFAULT_AI_ROUTES', () => { + expect(DEFAULT_AI_ROUTES.prefix).toBe('/api/v1/ai'); + expect(DEFAULT_AI_ROUTES.service).toBe('ai'); + expect(DEFAULT_AI_ROUTES.category).toBe('ai'); + expect(DEFAULT_AI_ROUTES.methods).toContain('aiNlq'); + expect(DEFAULT_AI_ROUTES.methods).toContain('aiChat'); + expect(DEFAULT_AI_ROUTES.methods).toContain('aiSuggest'); + expect(DEFAULT_AI_ROUTES.methods).toContain('aiInsights'); + expect(DEFAULT_AI_ROUTES.endpoints).toHaveLength(4); + // AI endpoints should have extended timeouts + const chatEndpoint = DEFAULT_AI_ROUTES.endpoints?.find(e => e.handler === 'aiChat'); + expect(chatEndpoint?.timeout).toBe(60000); + }); + + it('should validate DEFAULT_I18N_ROUTES', () => { + expect(DEFAULT_I18N_ROUTES.prefix).toBe('/api/v1/i18n'); + expect(DEFAULT_I18N_ROUTES.service).toBe('i18n'); + expect(DEFAULT_I18N_ROUTES.category).toBe('i18n'); + expect(DEFAULT_I18N_ROUTES.methods).toContain('getLocales'); + expect(DEFAULT_I18N_ROUTES.methods).toContain('getTranslations'); + expect(DEFAULT_I18N_ROUTES.methods).toContain('getFieldLabels'); + expect(DEFAULT_I18N_ROUTES.endpoints).toHaveLength(3); + // i18n endpoints should be cacheable + DEFAULT_I18N_ROUTES.endpoints?.forEach(endpoint => { + expect(endpoint.cacheable).toBe(true); + }); + }); + + it('should validate DEFAULT_ANALYTICS_ROUTES', () => { + expect(DEFAULT_ANALYTICS_ROUTES.prefix).toBe('/api/v1/analytics'); + expect(DEFAULT_ANALYTICS_ROUTES.service).toBe('analytics'); + expect(DEFAULT_ANALYTICS_ROUTES.category).toBe('analytics'); + expect(DEFAULT_ANALYTICS_ROUTES.methods).toContain('analyticsQuery'); + expect(DEFAULT_ANALYTICS_ROUTES.methods).toContain('getAnalyticsMeta'); + expect(DEFAULT_ANALYTICS_ROUTES.endpoints).toHaveLength(2); + // Analytics query should have extended timeout + const queryEndpoint = DEFAULT_ANALYTICS_ROUTES.endpoints?.find(e => e.handler === 'analyticsQuery'); + expect(queryEndpoint?.timeout).toBe(120000); + }); + + it('should validate DEFAULT_HUB_ROUTES', () => { + expect(DEFAULT_HUB_ROUTES.prefix).toBe('/api/v1/hub'); + expect(DEFAULT_HUB_ROUTES.service).toBe('hub'); + expect(DEFAULT_HUB_ROUTES.category).toBe('hub'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('listSpaces'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('createSpace'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('installPlugin'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('listPackages'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('installPackage'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('uninstallPackage'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('enablePackage'); + expect(DEFAULT_HUB_ROUTES.methods).toContain('disablePackage'); + expect(DEFAULT_HUB_ROUTES.endpoints).toHaveLength(9); + }); + + it('should validate DEFAULT_AUTOMATION_ROUTES', () => { + expect(DEFAULT_AUTOMATION_ROUTES.prefix).toBe('/api/v1/automation'); + expect(DEFAULT_AUTOMATION_ROUTES.service).toBe('automation'); + expect(DEFAULT_AUTOMATION_ROUTES.category).toBe('automation'); + expect(DEFAULT_AUTOMATION_ROUTES.methods).toContain('triggerAutomation'); + expect(DEFAULT_AUTOMATION_ROUTES.endpoints).toHaveLength(1); + // Automation trigger should have extended timeout + expect(DEFAULT_AUTOMATION_ROUTES.endpoints?.[0].timeout).toBe(120000); + }); + + it('should return all 14 default registrations', () => { + const registrations = getDefaultRouteRegistrations(); + + expect(registrations).toHaveLength(14); + expect(registrations[0]).toBe(DEFAULT_DISCOVERY_ROUTES); + expect(registrations[1]).toBe(DEFAULT_METADATA_ROUTES); + expect(registrations[2]).toBe(DEFAULT_DATA_CRUD_ROUTES); + expect(registrations[3]).toBe(DEFAULT_BATCH_ROUTES); + expect(registrations[4]).toBe(DEFAULT_PERMISSION_ROUTES); + expect(registrations[5]).toBe(DEFAULT_VIEW_ROUTES); + expect(registrations[6]).toBe(DEFAULT_WORKFLOW_ROUTES); + expect(registrations[7]).toBe(DEFAULT_REALTIME_ROUTES); + expect(registrations[8]).toBe(DEFAULT_NOTIFICATION_ROUTES); + expect(registrations[9]).toBe(DEFAULT_AI_ROUTES); + expect(registrations[10]).toBe(DEFAULT_I18N_ROUTES); + expect(registrations[11]).toBe(DEFAULT_ANALYTICS_ROUTES); + expect(registrations[12]).toBe(DEFAULT_HUB_ROUTES); + expect(registrations[13]).toBe(DEFAULT_AUTOMATION_ROUTES); + }); + + it('should cover all protocol categories', () => { + const registrations = getDefaultRouteRegistrations(); + const categories = registrations.map(r => r.category); + + expect(categories).toContain('discovery'); + expect(categories).toContain('metadata'); + expect(categories).toContain('data'); + expect(categories).toContain('batch'); + expect(categories).toContain('permission'); + expect(categories).toContain('ui'); + expect(categories).toContain('workflow'); + expect(categories).toContain('realtime'); + expect(categories).toContain('notification'); + expect(categories).toContain('ai'); + expect(categories).toContain('i18n'); + expect(categories).toContain('analytics'); + expect(categories).toContain('hub'); + expect(categories).toContain('automation'); + }); + }); + + describe('Schema Consistency', () => { + it('should ensure all endpoints have required fields', () => { + const allRegistrations = getDefaultRouteRegistrations(); + + allRegistrations.forEach(registration => { + registration.endpoints?.forEach(endpoint => { + expect(endpoint.method).toBeDefined(); + expect(endpoint.path).toBeDefined(); + expect(endpoint.handler).toBeDefined(); + expect(endpoint.category).toBeDefined(); + // public field has a default value of false, so it's always defined after parsing + expect(typeof endpoint.public).toBe('boolean'); + }); + }); + }); + + it('should ensure middleware has proper order', () => { + const allRegistrations = getDefaultRouteRegistrations(); + + allRegistrations.forEach(registration => { + if (registration.middleware && registration.middleware.length > 1) { + for (let i = 1; i < registration.middleware.length; i++) { + const prev = registration.middleware[i - 1]; + const curr = registration.middleware[i]; + + // If order is specified, ensure it's increasing + if (prev.order && curr.order) { + expect(curr.order).toBeGreaterThanOrEqual(prev.order); + } + } + } + }); + }); + + it('should ensure auth middleware comes before validation', () => { + const allRegistrations = getDefaultRouteRegistrations(); + + allRegistrations.forEach(registration => { + if (registration.middleware) { + const authIndex = registration.middleware.findIndex(m => m.type === 'authentication'); + const validationIndex = registration.middleware.findIndex(m => m.type === 'validation'); + + if (authIndex !== -1 && validationIndex !== -1) { + expect(authIndex).toBeLessThan(validationIndex); + } + } + }); + }); + }); +}); diff --git a/packages/spec/src/api/plugin-rest-api.zod.ts b/packages/spec/src/api/plugin-rest-api.zod.ts new file mode 100644 index 000000000..9aa5a9e8a --- /dev/null +++ b/packages/spec/src/api/plugin-rest-api.zod.ts @@ -0,0 +1,1820 @@ +import { z } from 'zod'; +import { HttpMethod } from '../shared/http.zod'; +import { MiddlewareConfigSchema } from '../system/http-server.zod'; + +/** + * REST API Plugin Protocol + * + * Defines the schema for REST API plugins that register Discovery, Metadata, + * Data CRUD, Batch, and Permission routes with the HTTP Dispatcher. + * + * This plugin type implements Phase 2 of the API Protocol implementation plan, + * providing standardized REST endpoints with: + * - Request validation middleware using Zod schemas + * - Response envelope wrapping with BaseResponseSchema + * - Error handling using ApiErrorSchema + * - OpenAPI documentation auto-generation + * + * Features: + * - Route registration for core API endpoints + * - Automatic schema-based validation + * - Standardized request/response envelopes + * - OpenAPI/Swagger documentation generation + * + * Architecture Alignment: + * - Salesforce: REST API with metadata and data CRUD + * - Microsoft Dynamics: Web API with entity operations + * - Strapi: Auto-generated REST endpoints from schemas + * + * @example Plugin Manifest + * ```typescript + * { + * "name": "rest_api", + * "version": "1.0.0", + * "type": "server", + * "contributes": { + * "routes": [ + * { + * "prefix": "/api/v1/discovery", + * "service": "metadata", + * "methods": ["getDiscovery"], + * "middleware": [ + * { "name": "response_envelope", "type": "transformation", "enabled": true } + * ] + * }, + * { + * "prefix": "/api/v1/meta", + * "service": "metadata", + * "methods": ["getMetaTypes", "getMetaItems", "getMetaItem", "saveMetaItem"], + * "middleware": [ + * { "name": "auth", "type": "authentication", "enabled": true }, + * { "name": "request_validation", "type": "validation", "enabled": true } + * ] + * }, + * { + * "prefix": "/api/v1/data", + * "service": "data", + * "methods": ["findData", "getData", "createData", "updateData", "deleteData"] + * } + * ] + * } + * } + * ``` + */ + +// ========================================== +// REST API Route Categories +// ========================================== + +/** + * REST API Route Category Enum + * Categorizes REST API routes by their primary function + */ +export const RestApiRouteCategory = z.enum([ + 'discovery', // API discovery and capabilities + 'metadata', // Metadata operations (objects, fields, views) + 'data', // Data CRUD operations + 'batch', // Batch/bulk operations + 'permission', // Permission/authorization checks + 'analytics', // Analytics and reporting + 'automation', // Automation triggers and flows + 'workflow', // Workflow state management + 'ui', // UI metadata (views, layouts) + 'realtime', // Realtime/WebSocket + 'notification', // Notification management + 'ai', // AI operations (NLQ, chat) + 'i18n', // Internationalization + 'hub', // Hub and package management +]); + +export type RestApiRouteCategory = z.infer; + +// ========================================== +// Route Registration Schema +// ========================================== + +/** + * REST API Endpoint Schema + * Defines a single REST API endpoint with its metadata + * + * @example Discovery Endpoint + * { + * "method": "GET", + * "path": "/api/v1/discovery", + * "handler": "getDiscovery", + * "category": "discovery", + * "public": true, + * "description": "Get API discovery information" + * } + */ +export const RestApiEndpointSchema = z.object({ + /** + * HTTP method + */ + method: HttpMethod.describe('HTTP method for this endpoint'), + + /** + * URL path pattern (supports parameters like :id) + */ + path: z.string().describe('URL path pattern (e.g., /api/v1/data/:object/:id)'), + + /** + * Handler reference (protocol method name) + */ + handler: z.string().describe('Protocol method name or handler identifier'), + + /** + * Route category + */ + category: RestApiRouteCategory.describe('Route category'), + + /** + * Whether endpoint is publicly accessible (no auth required) + */ + public: z.boolean().default(false).describe('Is publicly accessible without authentication'), + + /** + * Required permissions + */ + permissions: z.array(z.string()).optional().describe('Required permissions (e.g., ["data.read", "object.account.read"])'), + + /** + * OpenAPI documentation metadata + */ + summary: z.string().optional().describe('Short description for OpenAPI'), + description: z.string().optional().describe('Detailed description for OpenAPI'), + tags: z.array(z.string()).optional().describe('OpenAPI tags for grouping'), + + /** + * Request/Response schema references + */ + requestSchema: z.string().optional().describe('Request schema name (for validation)'), + responseSchema: z.string().optional().describe('Response schema name (for documentation)'), + + /** + * Performance and reliability settings + */ + timeout: z.number().int().optional().describe('Request timeout in milliseconds'), + rateLimit: z.string().optional().describe('Rate limit policy name'), + cacheable: z.boolean().default(false).describe('Whether response can be cached'), + cacheTtl: z.number().int().optional().describe('Cache TTL in seconds'), +}); + +export type RestApiEndpoint = z.infer; + +/** + * REST API Route Registration Schema + * Registers a group of related endpoints under a common prefix + * + * @example Data CRUD Routes + * { + * "prefix": "/api/v1/data", + * "service": "data", + * "category": "data", + * "endpoints": [ + * { "method": "GET", "path": "/:object", "handler": "findData" }, + * { "method": "GET", "path": "/:object/:id", "handler": "getData" }, + * { "method": "POST", "path": "/:object", "handler": "createData" }, + * { "method": "PATCH", "path": "/:object/:id", "handler": "updateData" }, + * { "method": "DELETE", "path": "/:object/:id", "handler": "deleteData" } + * ], + * "middleware": [ + * { "name": "auth", "type": "authentication", "enabled": true }, + * { "name": "validation", "type": "validation", "enabled": true }, + * { "name": "response_envelope", "type": "transformation", "enabled": true } + * ] + * } + */ +export const RestApiRouteRegistrationSchema = z.object({ + /** + * URL prefix for this route group (e.g., /api/v1/data) + */ + prefix: z.string().regex(/^\//).describe('URL path prefix for this route group'), + + /** + * Service name that handles these routes + */ + service: z.string().describe('Core service name (metadata, data, auth, etc.)'), + + /** + * Route category + */ + category: RestApiRouteCategory.describe('Primary category for this route group'), + + /** + * Protocol methods implemented + */ + methods: z.array(z.string()).optional().describe('Protocol method names implemented'), + + /** + * Detailed endpoint definitions + */ + endpoints: z.array(RestApiEndpointSchema).optional().describe('Endpoint definitions'), + + /** + * Middleware applied to all routes in this group + */ + middleware: z.array(MiddlewareConfigSchema).optional().describe('Middleware stack for this route group'), + + /** + * Whether authentication is required for all routes + */ + authRequired: z.boolean().default(true).describe('Whether authentication is required by default'), + + /** + * OpenAPI documentation + */ + documentation: z.object({ + title: z.string().optional().describe('Route group title'), + description: z.string().optional().describe('Route group description'), + tags: z.array(z.string()).optional().describe('OpenAPI tags'), + }).optional().describe('Documentation metadata for this route group'), +}); + +export type RestApiRouteRegistration = z.infer; + +// ========================================== +// Request Validation Configuration +// ========================================== + +/** + * Request Validation Mode Enum + * Defines how validation errors are handled + */ +export const ValidationMode = z.enum([ + 'strict', // Reject requests with validation errors (400 Bad Request) + 'permissive', // Log validation errors but allow request to proceed + 'strip', // Remove invalid fields and continue with valid data +]); + +export type ValidationMode = z.infer; + +/** + * Request Validation Configuration Schema + * Configures Zod-based request validation middleware + * + * @example + * { + * "enabled": true, + * "mode": "strict", + * "validateBody": true, + * "validateQuery": true, + * "validateParams": true, + * "includeFieldErrors": true + * } + */ +export const RequestValidationConfigSchema = z.object({ + /** + * Enable request validation + */ + enabled: z.boolean().default(true).describe('Enable automatic request validation'), + + /** + * Validation mode + */ + mode: ValidationMode.default('strict').describe('How to handle validation errors'), + + /** + * Validate request body + */ + validateBody: z.boolean().default(true).describe('Validate request body against schema'), + + /** + * Validate query parameters + */ + validateQuery: z.boolean().default(true).describe('Validate query string parameters'), + + /** + * Validate URL parameters + */ + validateParams: z.boolean().default(true).describe('Validate URL path parameters'), + + /** + * Validate request headers + */ + validateHeaders: z.boolean().default(false).describe('Validate request headers'), + + /** + * Include detailed field errors in response + */ + includeFieldErrors: z.boolean().default(true).describe('Include field-level error details in response'), + + /** + * Custom error message prefix + */ + errorPrefix: z.string().optional().describe('Custom prefix for validation error messages'), + + /** + * Schema registry reference + */ + schemaRegistry: z.string().optional().describe('Schema registry name to use for validation'), +}); + +export type RequestValidationConfig = z.infer; +export type RequestValidationConfigInput = z.input; + +// ========================================== +// Response Envelope Configuration +// ========================================== + +/** + * Response Envelope Configuration Schema + * Configures automatic response wrapping with BaseResponseSchema + * + * @example + * { + * "enabled": true, + * "includeMetadata": true, + * "includeTimestamp": true, + * "includeRequestId": true, + * "includeDuration": true + * } + */ +export const ResponseEnvelopeConfigSchema = z.object({ + /** + * Enable response envelope wrapping + */ + enabled: z.boolean().default(true).describe('Enable automatic response envelope wrapping'), + + /** + * Include metadata object + */ + includeMetadata: z.boolean().default(true).describe('Include meta object in responses'), + + /** + * Include timestamp in metadata + */ + includeTimestamp: z.boolean().default(true).describe('Include timestamp in response metadata'), + + /** + * Include request ID in metadata + */ + includeRequestId: z.boolean().default(true).describe('Include requestId in response metadata'), + + /** + * Include request duration in metadata + */ + includeDuration: z.boolean().default(false).describe('Include request duration in ms'), + + /** + * Include trace ID for distributed tracing + */ + includeTraceId: z.boolean().default(false).describe('Include distributed traceId'), + + /** + * Custom metadata fields + */ + customMetadata: z.record(z.string(), z.unknown()).optional().describe('Additional metadata fields to include'), + + /** + * Whether to wrap already-wrapped responses + */ + skipIfWrapped: z.boolean().default(true).describe('Skip wrapping if response already has success field'), +}); + +export type ResponseEnvelopeConfig = z.infer; +export type ResponseEnvelopeConfigInput = z.input; + +// ========================================== +// Error Handling Configuration +// ========================================== + +/** + * Error Handling Configuration Schema + * Configures error handling and ApiErrorSchema formatting + * + * @example + * { + * "enabled": true, + * "includeStackTrace": false, + * "logErrors": true, + * "exposeInternalErrors": false, + * "customErrorMessages": { + * "validation_error": "The request data is invalid. Please check your input." + * } + * } + */ +export const ErrorHandlingConfigSchema = z.object({ + /** + * Enable standardized error handling + */ + enabled: z.boolean().default(true).describe('Enable standardized error handling'), + + /** + * Include stack traces in error responses (dev only) + */ + includeStackTrace: z.boolean().default(false).describe('Include stack traces in error responses'), + + /** + * Log errors to logger + */ + logErrors: z.boolean().default(true).describe('Log errors to system logger'), + + /** + * Expose internal error details + */ + exposeInternalErrors: z.boolean().default(false).describe('Expose internal error details in responses'), + + /** + * Include request ID in errors + */ + includeRequestId: z.boolean().default(true).describe('Include requestId in error responses'), + + /** + * Include timestamp in errors + */ + includeTimestamp: z.boolean().default(true).describe('Include timestamp in error responses'), + + /** + * Include error documentation URLs + */ + includeDocumentation: z.boolean().default(true).describe('Include documentation URLs for errors'), + + /** + * Documentation base URL + */ + documentationBaseUrl: z.string().url().optional().describe('Base URL for error documentation'), + + /** + * Custom error messages by code + */ + customErrorMessages: z.record(z.string(), z.string()).optional() + .describe('Custom error messages by error code'), + + /** + * Sensitive fields to redact from error details + */ + redactFields: z.array(z.string()).optional().describe('Field names to redact from error details'), +}); + +export type ErrorHandlingConfig = z.infer; +export type ErrorHandlingConfigInput = z.input; + +// ========================================== +// OpenAPI Documentation Configuration +// ========================================== + +/** + * OpenAPI Generation Configuration Schema + * Configures automatic OpenAPI documentation generation + * + * @example + * { + * "enabled": true, + * "version": "3.0.0", + * "title": "ObjectStack API", + * "description": "ObjectStack REST API", + * "outputPath": "/api/docs/openapi.json", + * "uiPath": "/api/docs", + * "includeInternal": false, + * "generateSchemas": true + * } + */ +export const OpenApiGenerationConfigSchema = z.object({ + /** + * Enable OpenAPI generation + */ + enabled: z.boolean().default(true).describe('Enable automatic OpenAPI documentation generation'), + + /** + * OpenAPI specification version + */ + version: z.enum(['3.0.0', '3.0.1', '3.0.2', '3.0.3', '3.1.0']).default('3.0.3') + .describe('OpenAPI specification version'), + + /** + * API title + */ + title: z.string().default('ObjectStack API').describe('API title'), + + /** + * API description + */ + description: z.string().optional().describe('API description'), + + /** + * API version + */ + apiVersion: z.string().default('1.0.0').describe('API version'), + + /** + * Output path for OpenAPI spec + */ + outputPath: z.string().default('/api/docs/openapi.json').describe('URL path to serve OpenAPI JSON'), + + /** + * UI path for Swagger/Redoc + */ + uiPath: z.string().default('/api/docs').describe('URL path to serve documentation UI'), + + /** + * UI framework to use + */ + uiFramework: z.enum(['swagger-ui', 'redoc', 'rapidoc', 'elements']).default('swagger-ui') + .describe('Documentation UI framework'), + + /** + * Include internal/admin endpoints + */ + includeInternal: z.boolean().default(false).describe('Include internal endpoints in documentation'), + + /** + * Generate JSON schemas from Zod + */ + generateSchemas: z.boolean().default(true).describe('Auto-generate schemas from Zod definitions'), + + /** + * Include examples in documentation + */ + includeExamples: z.boolean().default(true).describe('Include request/response examples'), + + /** + * Server URLs + */ + servers: z.array(z.object({ + url: z.string().describe('Server URL'), + description: z.string().optional().describe('Server description'), + })).optional().describe('Server URLs for API'), + + /** + * Contact information + */ + contact: z.object({ + name: z.string().optional(), + url: z.string().url().optional(), + email: z.string().email().optional(), + }).optional().describe('API contact information'), + + /** + * License information + */ + license: z.object({ + name: z.string().describe('License name'), + url: z.string().url().optional().describe('License URL'), + }).optional().describe('API license information'), + + /** + * Security schemes + */ + securitySchemes: z.record(z.string(), z.object({ + type: z.enum(['apiKey', 'http', 'oauth2', 'openIdConnect']), + scheme: z.string().optional(), + bearerFormat: z.string().optional(), + })).optional().describe('Security scheme definitions'), +}); + +export type OpenApiGenerationConfig = z.infer; +export type OpenApiGenerationConfigInput = z.input; + +// ========================================== +// REST API Plugin Configuration +// ========================================== + +/** + * REST API Plugin Configuration Schema + * Complete configuration for REST API plugin + * + * @example + * { + * "enabled": true, + * "basePath": "/api", + * "version": "v1", + * "routes": [...], + * "validation": { "enabled": true, "mode": "strict" }, + * "responseEnvelope": { "enabled": true, "includeMetadata": true }, + * "errorHandling": { "enabled": true, "includeStackTrace": false }, + * "openApi": { "enabled": true, "title": "ObjectStack API" } + * } + */ +export const RestApiPluginConfigSchema = z.object({ + /** + * Enable REST API plugin + */ + enabled: z.boolean().default(true).describe('Enable REST API plugin'), + + /** + * API base path + */ + basePath: z.string().default('/api').describe('Base path for all API routes'), + + /** + * API version + */ + version: z.string().default('v1').describe('API version identifier'), + + /** + * Route registrations + */ + routes: z.array(RestApiRouteRegistrationSchema).describe('Route registrations'), + + /** + * Request validation configuration + */ + validation: RequestValidationConfigSchema.optional().describe('Request validation configuration'), + + /** + * Response envelope configuration + */ + responseEnvelope: ResponseEnvelopeConfigSchema.optional().describe('Response envelope configuration'), + + /** + * Error handling configuration + */ + errorHandling: ErrorHandlingConfigSchema.optional().describe('Error handling configuration'), + + /** + * OpenAPI documentation configuration + */ + openApi: OpenApiGenerationConfigSchema.optional().describe('OpenAPI documentation configuration'), + + /** + * Global middleware applied to all routes + */ + globalMiddleware: z.array(MiddlewareConfigSchema).optional().describe('Global middleware stack'), + + /** + * CORS configuration + */ + cors: z.object({ + enabled: z.boolean().default(true), + origins: z.array(z.string()).optional(), + methods: z.array(HttpMethod).optional(), + credentials: z.boolean().default(true), + }).optional().describe('CORS configuration'), + + /** + * Performance settings + */ + performance: z.object({ + enableCompression: z.boolean().default(true).describe('Enable response compression'), + enableETag: z.boolean().default(true).describe('Enable ETag generation'), + enableCaching: z.boolean().default(true).describe('Enable HTTP caching'), + defaultCacheTtl: z.number().int().default(300).describe('Default cache TTL in seconds'), + }).optional().describe('Performance optimization settings'), +}); + +export type RestApiPluginConfig = z.infer; +export type RestApiPluginConfigInput = z.input; + +// ========================================== +// Default Route Registrations +// ========================================== + +/** + * Default Discovery Routes + * Standard routes for API discovery endpoint + */ +export const DEFAULT_DISCOVERY_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/discovery', + service: 'metadata', + category: 'discovery', + methods: ['getDiscovery'], + authRequired: false, + endpoints: [{ + method: 'GET', + path: '', + handler: 'getDiscovery', + category: 'discovery', + public: true, + summary: 'Get API discovery information', + description: 'Returns API version, capabilities, and available routes', + tags: ['Discovery'], + responseSchema: 'GetDiscoveryResponseSchema', + cacheable: true, + cacheTtl: 3600, // Cache for 1 hour as discovery info rarely changes + }], + middleware: [ + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +/** + * Default Metadata Routes + * Standard routes for metadata operations + * + * Note: getMetaItemCached is not a separate endpoint - it's handled by the getMetaItem + * endpoint with HTTP cache headers (ETag, If-None-Match, etc.) for conditional requests. + */ +export const DEFAULT_METADATA_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/meta', + service: 'metadata', + category: 'metadata', + methods: ['getMetaTypes', 'getMetaItems', 'getMetaItem', 'saveMetaItem'], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '', + handler: 'getMetaTypes', + category: 'metadata', + public: false, + summary: 'List all metadata types', + description: 'Returns available metadata types (object, field, view, etc.)', + tags: ['Metadata'], + responseSchema: 'GetMetaTypesResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + { + method: 'GET', + path: '/:type', + handler: 'getMetaItems', + category: 'metadata', + public: false, + summary: 'List metadata items of a type', + description: 'Returns all items of the specified metadata type', + tags: ['Metadata'], + responseSchema: 'GetMetaItemsResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + { + method: 'GET', + path: '/:type/:name', + handler: 'getMetaItem', + category: 'metadata', + public: false, + summary: 'Get specific metadata item', + description: 'Returns a specific metadata item by type and name', + tags: ['Metadata'], + requestSchema: 'GetMetaItemRequestSchema', + responseSchema: 'GetMetaItemResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + { + method: 'PUT', + path: '/:type/:name', + handler: 'saveMetaItem', + category: 'metadata', + public: false, + summary: 'Create or update metadata item', + description: 'Creates or updates a metadata item', + tags: ['Metadata'], + requestSchema: 'SaveMetaItemRequestSchema', + responseSchema: 'SaveMetaItemResponseSchema', + permissions: ['metadata.write'], + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +/** + * Default Data CRUD Routes + * Standard routes for data operations + */ +export const DEFAULT_DATA_CRUD_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/data', + service: 'data', + category: 'data', + methods: ['findData', 'getData', 'createData', 'updateData', 'deleteData'], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '/:object', + handler: 'findData', + category: 'data', + public: false, + summary: 'Query records', + description: 'Query records with filtering, sorting, and pagination', + tags: ['Data'], + requestSchema: 'FindDataRequestSchema', + responseSchema: 'ListRecordResponseSchema', + permissions: ['data.read'], + cacheable: false, + }, + { + method: 'GET', + path: '/:object/:id', + handler: 'getData', + category: 'data', + public: false, + summary: 'Get record by ID', + description: 'Retrieve a single record by its ID', + tags: ['Data'], + requestSchema: 'IdRequestSchema', + responseSchema: 'SingleRecordResponseSchema', + permissions: ['data.read'], + cacheable: false, + }, + { + method: 'POST', + path: '/:object', + handler: 'createData', + category: 'data', + public: false, + summary: 'Create record', + description: 'Create a new record', + tags: ['Data'], + requestSchema: 'CreateRequestSchema', + responseSchema: 'SingleRecordResponseSchema', + permissions: ['data.create'], + cacheable: false, + }, + { + method: 'PATCH', + path: '/:object/:id', + handler: 'updateData', + category: 'data', + public: false, + summary: 'Update record', + description: 'Update an existing record', + tags: ['Data'], + requestSchema: 'UpdateRequestSchema', + responseSchema: 'SingleRecordResponseSchema', + permissions: ['data.update'], + cacheable: false, + }, + { + method: 'DELETE', + path: '/:object/:id', + handler: 'deleteData', + category: 'data', + public: false, + summary: 'Delete record', + description: 'Delete a record by ID', + tags: ['Data'], + requestSchema: 'IdRequestSchema', + responseSchema: 'DeleteResponseSchema', + permissions: ['data.delete'], + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +/** + * Default Batch Routes + * Standard routes for batch operations + */ +export const DEFAULT_BATCH_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/data/:object', + service: 'data', + category: 'batch', + methods: ['batchData', 'createManyData', 'updateManyData', 'deleteManyData'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/batch', + handler: 'batchData', + category: 'batch', + public: false, + summary: 'Batch operation', + description: 'Execute a batch operation (create, update, upsert, delete)', + tags: ['Batch'], + requestSchema: 'BatchUpdateRequestSchema', + responseSchema: 'BatchUpdateResponseSchema', + permissions: ['data.batch'], + timeout: 60000, // 60 seconds for batch operations + cacheable: false, + }, + { + method: 'POST', + path: '/createMany', + handler: 'createManyData', + category: 'batch', + public: false, + summary: 'Batch create', + description: 'Create multiple records in a single operation', + tags: ['Batch'], + requestSchema: 'CreateManyRequestSchema', + responseSchema: 'BatchUpdateResponseSchema', + permissions: ['data.create', 'data.batch'], + timeout: 60000, + cacheable: false, + }, + { + method: 'POST', + path: '/updateMany', + handler: 'updateManyData', + category: 'batch', + public: false, + summary: 'Batch update', + description: 'Update multiple records in a single operation', + tags: ['Batch'], + requestSchema: 'UpdateManyRequestSchema', + responseSchema: 'BatchUpdateResponseSchema', + permissions: ['data.update', 'data.batch'], + timeout: 60000, + cacheable: false, + }, + { + method: 'POST', + path: '/deleteMany', + handler: 'deleteManyData', + category: 'batch', + public: false, + summary: 'Batch delete', + description: 'Delete multiple records in a single operation', + tags: ['Batch'], + requestSchema: 'DeleteManyRequestSchema', + responseSchema: 'BatchUpdateResponseSchema', + permissions: ['data.delete', 'data.batch'], + timeout: 60000, + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +/** + * Default Permission Routes + * Standard routes for permission checking + */ +export const DEFAULT_PERMISSION_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/auth', + service: 'auth', + category: 'permission', + methods: ['checkPermission', 'getObjectPermissions', 'getEffectivePermissions'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/check', + handler: 'checkPermission', + category: 'permission', + public: false, + summary: 'Check permission', + description: 'Check if current user has a specific permission', + tags: ['Permission'], + requestSchema: 'CheckPermissionRequestSchema', + responseSchema: 'CheckPermissionResponseSchema', + cacheable: false, + }, + { + method: 'GET', + path: '/permissions/:object', + handler: 'getObjectPermissions', + category: 'permission', + public: false, + summary: 'Get object permissions', + description: 'Get all permissions for a specific object', + tags: ['Permission'], + responseSchema: 'ObjectPermissionsResponseSchema', + cacheable: true, + cacheTtl: 300, + }, + { + method: 'GET', + path: '/permissions/effective', + handler: 'getEffectivePermissions', + category: 'permission', + public: false, + summary: 'Get effective permissions', + description: 'Get all effective permissions for current user', + tags: ['Permission'], + responseSchema: 'EffectivePermissionsResponseSchema', + cacheable: true, + cacheTtl: 300, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +// ========================================== +// View Management Routes +// ========================================== + +/** + * Default View Management Routes + * Standard routes for UI view CRUD operations + */ +export const DEFAULT_VIEW_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/ui', + service: 'ui', + category: 'ui', + methods: ['listViews', 'getView', 'createView', 'updateView', 'deleteView'], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '/views/:object', + handler: 'listViews', + category: 'ui', + public: false, + summary: 'List views for an object', + description: 'Returns all views (list, form) for the specified object', + tags: ['Views', 'UI'], + responseSchema: 'ListViewsResponseSchema', + cacheable: true, + cacheTtl: 1800, + }, + { + method: 'GET', + path: '/views/:object/:viewId', + handler: 'getView', + category: 'ui', + public: false, + summary: 'Get a specific view', + description: 'Returns a specific view definition by object and view ID', + tags: ['Views', 'UI'], + responseSchema: 'GetViewResponseSchema', + cacheable: true, + cacheTtl: 1800, + }, + { + method: 'POST', + path: '/views/:object', + handler: 'createView', + category: 'ui', + public: false, + summary: 'Create a new view', + description: 'Creates a new view definition for the specified object', + tags: ['Views', 'UI'], + requestSchema: 'CreateViewRequestSchema', + responseSchema: 'CreateViewResponseSchema', + permissions: ['ui.view.create'], + cacheable: false, + }, + { + method: 'PATCH', + path: '/views/:object/:viewId', + handler: 'updateView', + category: 'ui', + public: false, + summary: 'Update a view', + description: 'Updates an existing view definition', + tags: ['Views', 'UI'], + requestSchema: 'UpdateViewRequestSchema', + responseSchema: 'UpdateViewResponseSchema', + permissions: ['ui.view.update'], + cacheable: false, + }, + { + method: 'DELETE', + path: '/views/:object/:viewId', + handler: 'deleteView', + category: 'ui', + public: false, + summary: 'Delete a view', + description: 'Deletes a view definition', + tags: ['Views', 'UI'], + responseSchema: 'DeleteViewResponseSchema', + permissions: ['ui.view.delete'], + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +// ========================================== +// Workflow Routes +// ========================================== + +/** + * Default Workflow Routes + * Standard routes for workflow state management and transitions + */ +export const DEFAULT_WORKFLOW_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/workflow', + service: 'workflow', + category: 'workflow', + methods: ['getWorkflowConfig', 'getWorkflowState', 'workflowTransition', 'workflowApprove', 'workflowReject'], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '/:object/config', + handler: 'getWorkflowConfig', + category: 'workflow', + public: false, + summary: 'Get workflow configuration', + description: 'Returns workflow rules and state machine configuration for an object', + tags: ['Workflow'], + responseSchema: 'GetWorkflowConfigResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + { + method: 'GET', + path: '/:object/:recordId/state', + handler: 'getWorkflowState', + category: 'workflow', + public: false, + summary: 'Get workflow state', + description: 'Returns current workflow state and available transitions for a record', + tags: ['Workflow'], + responseSchema: 'GetWorkflowStateResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/:object/:recordId/transition', + handler: 'workflowTransition', + category: 'workflow', + public: false, + summary: 'Execute workflow transition', + description: 'Transitions a record to a new workflow state', + tags: ['Workflow'], + requestSchema: 'WorkflowTransitionRequestSchema', + responseSchema: 'WorkflowTransitionResponseSchema', + permissions: ['workflow.transition'], + cacheable: false, + }, + { + method: 'POST', + path: '/:object/:recordId/approve', + handler: 'workflowApprove', + category: 'workflow', + public: false, + summary: 'Approve workflow step', + description: 'Approves a pending workflow approval step', + tags: ['Workflow'], + requestSchema: 'WorkflowApproveRequestSchema', + responseSchema: 'WorkflowApproveResponseSchema', + permissions: ['workflow.approve'], + cacheable: false, + }, + { + method: 'POST', + path: '/:object/:recordId/reject', + handler: 'workflowReject', + category: 'workflow', + public: false, + summary: 'Reject workflow step', + description: 'Rejects a pending workflow approval step', + tags: ['Workflow'], + requestSchema: 'WorkflowRejectRequestSchema', + responseSchema: 'WorkflowRejectResponseSchema', + permissions: ['workflow.reject'], + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +// ========================================== +// Realtime Routes +// ========================================== + +/** + * Default Realtime Routes + * Standard routes for realtime connection management and subscriptions + */ +export const DEFAULT_REALTIME_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/realtime', + service: 'realtime', + category: 'realtime', + methods: ['realtimeConnect', 'realtimeDisconnect', 'realtimeSubscribe', 'realtimeUnsubscribe', 'setPresence', 'getPresence'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/connect', + handler: 'realtimeConnect', + category: 'realtime', + public: false, + summary: 'Establish realtime connection', + description: 'Negotiates a realtime connection (WebSocket/SSE) and returns connection details', + tags: ['Realtime'], + requestSchema: 'RealtimeConnectRequestSchema', + responseSchema: 'RealtimeConnectResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/disconnect', + handler: 'realtimeDisconnect', + category: 'realtime', + public: false, + summary: 'Close realtime connection', + description: 'Closes an active realtime connection', + tags: ['Realtime'], + requestSchema: 'RealtimeDisconnectRequestSchema', + responseSchema: 'RealtimeDisconnectResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/subscribe', + handler: 'realtimeSubscribe', + category: 'realtime', + public: false, + summary: 'Subscribe to channel', + description: 'Subscribes to a realtime channel for receiving events', + tags: ['Realtime'], + requestSchema: 'RealtimeSubscribeRequestSchema', + responseSchema: 'RealtimeSubscribeResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/unsubscribe', + handler: 'realtimeUnsubscribe', + category: 'realtime', + public: false, + summary: 'Unsubscribe from channel', + description: 'Unsubscribes from a realtime channel', + tags: ['Realtime'], + requestSchema: 'RealtimeUnsubscribeRequestSchema', + responseSchema: 'RealtimeUnsubscribeResponseSchema', + cacheable: false, + }, + { + method: 'PUT', + path: '/presence/:channel', + handler: 'setPresence', + category: 'realtime', + public: false, + summary: 'Set presence state', + description: 'Sets the current user\'s presence state in a channel', + tags: ['Realtime'], + requestSchema: 'SetPresenceRequestSchema', + responseSchema: 'SetPresenceResponseSchema', + cacheable: false, + }, + { + method: 'GET', + path: '/presence/:channel', + handler: 'getPresence', + category: 'realtime', + public: false, + summary: 'Get channel presence', + description: 'Returns all active members and their presence state in a channel', + tags: ['Realtime'], + responseSchema: 'GetPresenceResponseSchema', + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +// ========================================== +// Notification Routes +// ========================================== + +/** + * Default Notification Routes + * Standard routes for notification management (device registration, preferences, listing) + */ +export const DEFAULT_NOTIFICATION_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/notifications', + service: 'notification', + category: 'notification', + methods: [ + 'registerDevice', 'unregisterDevice', + 'getNotificationPreferences', 'updateNotificationPreferences', + 'listNotifications', 'markNotificationsRead', 'markAllNotificationsRead', + ], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/devices', + handler: 'registerDevice', + category: 'notification', + public: false, + summary: 'Register device for push notifications', + description: 'Registers a device token for receiving push notifications', + tags: ['Notifications'], + requestSchema: 'RegisterDeviceRequestSchema', + responseSchema: 'RegisterDeviceResponseSchema', + cacheable: false, + }, + { + method: 'DELETE', + path: '/devices/:deviceId', + handler: 'unregisterDevice', + category: 'notification', + public: false, + summary: 'Unregister device', + description: 'Removes a device from push notification registration', + tags: ['Notifications'], + responseSchema: 'UnregisterDeviceResponseSchema', + cacheable: false, + }, + { + method: 'GET', + path: '/preferences', + handler: 'getNotificationPreferences', + category: 'notification', + public: false, + summary: 'Get notification preferences', + description: 'Returns current user notification preferences', + tags: ['Notifications'], + responseSchema: 'GetNotificationPreferencesResponseSchema', + cacheable: false, + }, + { + method: 'PATCH', + path: '/preferences', + handler: 'updateNotificationPreferences', + category: 'notification', + public: false, + summary: 'Update notification preferences', + description: 'Updates user notification preferences', + tags: ['Notifications'], + requestSchema: 'UpdateNotificationPreferencesRequestSchema', + responseSchema: 'UpdateNotificationPreferencesResponseSchema', + cacheable: false, + }, + { + method: 'GET', + path: '', + handler: 'listNotifications', + category: 'notification', + public: false, + summary: 'List notifications', + description: 'Returns paginated list of notifications for the current user', + tags: ['Notifications'], + responseSchema: 'ListNotificationsResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/read', + handler: 'markNotificationsRead', + category: 'notification', + public: false, + summary: 'Mark notifications as read', + description: 'Marks specific notifications as read by their IDs', + tags: ['Notifications'], + requestSchema: 'MarkNotificationsReadRequestSchema', + responseSchema: 'MarkNotificationsReadResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/read/all', + handler: 'markAllNotificationsRead', + category: 'notification', + public: false, + summary: 'Mark all notifications as read', + description: 'Marks all notifications as read for the current user', + tags: ['Notifications'], + responseSchema: 'MarkAllNotificationsReadResponseSchema', + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +// ========================================== +// AI Routes +// ========================================== + +/** + * Default AI Routes + * Standard routes for AI operations (NLQ, Chat, Suggest, Insights) + */ +export const DEFAULT_AI_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/ai', + service: 'ai', + category: 'ai', + methods: ['aiNlq', 'aiChat', 'aiSuggest', 'aiInsights'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/nlq', + handler: 'aiNlq', + category: 'ai', + public: false, + summary: 'Natural language query', + description: 'Converts a natural language query to a structured query AST', + tags: ['AI'], + requestSchema: 'AiNlqRequestSchema', + responseSchema: 'AiNlqResponseSchema', + timeout: 30000, + cacheable: false, + }, + { + method: 'POST', + path: '/chat', + handler: 'aiChat', + category: 'ai', + public: false, + summary: 'AI chat interaction', + description: 'Sends a message to the AI assistant and receives a response', + tags: ['AI'], + requestSchema: 'AiChatRequestSchema', + responseSchema: 'AiChatResponseSchema', + timeout: 60000, + cacheable: false, + }, + { + method: 'POST', + path: '/suggest', + handler: 'aiSuggest', + category: 'ai', + public: false, + summary: 'Get AI-powered suggestions', + description: 'Returns AI-generated field value suggestions based on context', + tags: ['AI'], + requestSchema: 'AiSuggestRequestSchema', + responseSchema: 'AiSuggestResponseSchema', + timeout: 15000, + cacheable: false, + }, + { + method: 'POST', + path: '/insights', + handler: 'aiInsights', + category: 'ai', + public: false, + summary: 'Get AI-generated insights', + description: 'Returns AI-generated insights (summaries, trends, anomalies, recommendations)', + tags: ['AI'], + requestSchema: 'AiInsightsRequestSchema', + responseSchema: 'AiInsightsResponseSchema', + timeout: 60000, + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +// ========================================== +// i18n Routes +// ========================================== + +/** + * Default i18n Routes + * Standard routes for internationalization operations + */ +export const DEFAULT_I18N_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/i18n', + service: 'i18n', + category: 'i18n', + methods: ['getLocales', 'getTranslations', 'getFieldLabels'], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '/locales', + handler: 'getLocales', + category: 'i18n', + public: false, + summary: 'Get available locales', + description: 'Returns all available locales with their metadata', + tags: ['i18n'], + responseSchema: 'GetLocalesResponseSchema', + cacheable: true, + cacheTtl: 86400, // 24 hours — locales change very rarely + }, + { + method: 'GET', + path: '/translations/:locale', + handler: 'getTranslations', + category: 'i18n', + public: false, + summary: 'Get translations for a locale', + description: 'Returns translation strings for the specified locale and optional namespace', + tags: ['i18n'], + responseSchema: 'GetTranslationsResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + { + method: 'GET', + path: '/labels/:object/:locale', + handler: 'getFieldLabels', + category: 'i18n', + public: false, + summary: 'Get translated field labels', + description: 'Returns translated field labels, help text, and option labels for an object', + tags: ['i18n'], + responseSchema: 'GetFieldLabelsResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + ], +}; + +// ========================================== +// Analytics Routes +// ========================================== + +/** + * Default Analytics Routes + * Standard routes for analytics and BI operations + */ +export const DEFAULT_ANALYTICS_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/analytics', + service: 'analytics', + category: 'analytics', + methods: ['analyticsQuery', 'getAnalyticsMeta'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/query', + handler: 'analyticsQuery', + category: 'analytics', + public: false, + summary: 'Execute analytics query', + description: 'Executes a structured analytics query against the semantic layer', + tags: ['Analytics'], + requestSchema: 'AnalyticsQueryRequestSchema', + responseSchema: 'AnalyticsResultResponseSchema', + permissions: ['analytics.query'], + timeout: 120000, // 2 minutes for analytics queries + cacheable: false, + }, + { + method: 'GET', + path: '/meta', + handler: 'getAnalyticsMeta', + category: 'analytics', + public: false, + summary: 'Get analytics metadata', + description: 'Returns available cubes, dimensions, measures, and segments', + tags: ['Analytics'], + responseSchema: 'AnalyticsMetadataResponseSchema', + cacheable: true, + cacheTtl: 3600, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +// ========================================== +// Hub / Package Management Routes +// ========================================== + +/** + * Default Hub Routes + * Standard routes for hub management and package lifecycle + */ +export const DEFAULT_HUB_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/hub', + service: 'hub', + category: 'hub', + methods: [ + 'listSpaces', 'createSpace', 'installPlugin', + 'listPackages', 'getPackage', 'installPackage', 'uninstallPackage', + 'enablePackage', 'disablePackage', + ], + authRequired: true, + endpoints: [ + { + method: 'GET', + path: '/spaces', + handler: 'listSpaces', + category: 'hub', + public: false, + summary: 'List spaces', + description: 'Returns all hub spaces accessible to the current user', + tags: ['Hub'], + cacheable: true, + cacheTtl: 300, + }, + { + method: 'POST', + path: '/spaces', + handler: 'createSpace', + category: 'hub', + public: false, + summary: 'Create space', + description: 'Creates a new hub space', + tags: ['Hub'], + requestSchema: 'CreateSpaceRequestSchema', + responseSchema: 'SpaceResponseSchema', + permissions: ['hub.space.create'], + cacheable: false, + }, + { + method: 'POST', + path: '/plugins/install', + handler: 'installPlugin', + category: 'hub', + public: false, + summary: 'Install plugin into space', + description: 'Installs a plugin into the current space', + tags: ['Hub'], + requestSchema: 'InstallPluginRequestSchema', + responseSchema: 'InstallPluginResponseSchema', + permissions: ['hub.plugin.install'], + cacheable: false, + }, + { + method: 'GET', + path: '/packages', + handler: 'listPackages', + category: 'hub', + public: false, + summary: 'List installed packages', + description: 'Returns all installed packages with optional status filter', + tags: ['Hub', 'Packages'], + responseSchema: 'ListPackagesResponseSchema', + cacheable: false, + }, + { + method: 'GET', + path: '/packages/:id', + handler: 'getPackage', + category: 'hub', + public: false, + summary: 'Get package details', + description: 'Returns details of a specific installed package', + tags: ['Hub', 'Packages'], + responseSchema: 'GetPackageResponseSchema', + cacheable: false, + }, + { + method: 'POST', + path: '/packages', + handler: 'installPackage', + category: 'hub', + public: false, + summary: 'Install package', + description: 'Installs a new package from manifest or registry', + tags: ['Hub', 'Packages'], + requestSchema: 'InstallPackageRequestSchema', + responseSchema: 'InstallPackageResponseSchema', + permissions: ['hub.package.install'], + cacheable: false, + }, + { + method: 'DELETE', + path: '/packages/:id', + handler: 'uninstallPackage', + category: 'hub', + public: false, + summary: 'Uninstall package', + description: 'Removes an installed package', + tags: ['Hub', 'Packages'], + responseSchema: 'UninstallPackageResponseSchema', + permissions: ['hub.package.uninstall'], + cacheable: false, + }, + { + method: 'POST', + path: '/packages/:id/enable', + handler: 'enablePackage', + category: 'hub', + public: false, + summary: 'Enable package', + description: 'Enables a disabled package', + tags: ['Hub', 'Packages'], + responseSchema: 'EnablePackageResponseSchema', + permissions: ['hub.package.manage'], + cacheable: false, + }, + { + method: 'POST', + path: '/packages/:id/disable', + handler: 'disablePackage', + category: 'hub', + public: false, + summary: 'Disable package', + description: 'Disables an installed package without removing it', + tags: ['Hub', 'Packages'], + responseSchema: 'DisablePackageResponseSchema', + permissions: ['hub.package.manage'], + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +// ========================================== +// Automation Routes +// ========================================== + +/** + * Default Automation Routes + * Standard routes for automation triggers + */ +export const DEFAULT_AUTOMATION_ROUTES: RestApiRouteRegistration = { + prefix: '/api/v1/automation', + service: 'automation', + category: 'automation', + methods: ['triggerAutomation'], + authRequired: true, + endpoints: [ + { + method: 'POST', + path: '/trigger', + handler: 'triggerAutomation', + category: 'automation', + public: false, + summary: 'Trigger automation', + description: 'Triggers an automation flow or script by name', + tags: ['Automation'], + requestSchema: 'AutomationTriggerRequestSchema', + responseSchema: 'AutomationTriggerResponseSchema', + permissions: ['automation.trigger'], + timeout: 120000, // 2 minutes for long-running automations + cacheable: false, + }, + ], + middleware: [ + { name: 'auth', type: 'authentication', enabled: true, order: 10 }, + { name: 'validation', type: 'validation', enabled: true, order: 20 }, + { name: 'response_envelope', type: 'transformation', enabled: true, order: 100 }, + { name: 'error_handler', type: 'error', enabled: true, order: 200 }, + ], +}; + +// ========================================== +// Helper Functions +// ========================================== + +/** + * Helper to create REST API plugin configuration + */ +export const RestApiPluginConfig = Object.assign(RestApiPluginConfigSchema, { + create: >(config: T) => config, +}); + +/** + * Helper to create route registration + */ +export const RestApiRouteRegistration = Object.assign(RestApiRouteRegistrationSchema, { + create: >(registration: T) => registration, +}); + +/** + * Get all default route registrations. + * Returns the complete set of standard REST API routes covering all protocol namespaces. + * + * Route groups (13 total): + * 1. Discovery - API capabilities and routing info + * 2. Metadata - Object/field schema CRUD + * 3. Data CRUD - Record operations + * 4. Batch - Bulk operations + * 5. Permission - Authorization checks + * 6. Views - UI view CRUD + * 7. Workflow - State machine transitions + * 8. Realtime - WebSocket/SSE connections + * 9. Notification - Push notifications and preferences + * 10. AI - NLQ, chat, suggestions, insights + * 11. i18n - Locales and translations + * 12. Analytics - BI queries and metadata + * 13. Hub - Space and package management + * 14. Automation - Trigger flows and scripts + */ +export function getDefaultRouteRegistrations(): RestApiRouteRegistration[] { + return [ + DEFAULT_DISCOVERY_ROUTES, + DEFAULT_METADATA_ROUTES, + DEFAULT_DATA_CRUD_ROUTES, + DEFAULT_BATCH_ROUTES, + DEFAULT_PERMISSION_ROUTES, + DEFAULT_VIEW_ROUTES, + DEFAULT_WORKFLOW_ROUTES, + DEFAULT_REALTIME_ROUTES, + DEFAULT_NOTIFICATION_ROUTES, + DEFAULT_AI_ROUTES, + DEFAULT_I18N_ROUTES, + DEFAULT_ANALYTICS_ROUTES, + DEFAULT_HUB_ROUTES, + DEFAULT_AUTOMATION_ROUTES, + ]; +} diff --git a/packages/spec/src/api/router.zod.ts b/packages/spec/src/api/router.zod.ts index 3cb743564..4b49b606c 100644 --- a/packages/spec/src/api/router.zod.ts +++ b/packages/spec/src/api/router.zod.ts @@ -88,6 +88,13 @@ export const RouterConfigSchema = z.object({ analytics: z.string().default('/analytics').describe('Analytics Protocol'), hub: z.string().default('/hub').describe('Hub Management Protocol'), graphql: z.string().default('/graphql').describe('GraphQL Endpoint'), + ui: z.string().default('/ui').describe('UI Metadata Protocol (Views, Layouts)'), + workflow: z.string().default('/workflow').describe('Workflow Engine Protocol'), + realtime: z.string().default('/realtime').describe('Realtime/WebSocket Protocol'), + notifications: z.string().default('/notifications').describe('Notification Protocol'), + ai: z.string().default('/ai').describe('AI Engine Protocol (NLQ, Chat, Suggest)'), + i18n: z.string().default('/i18n').describe('Internationalization Protocol'), + packages: z.string().default('/packages').describe('Package Management Protocol'), }).default({ data: '/data', metadata: '/meta', @@ -96,7 +103,14 @@ export const RouterConfigSchema = z.object({ storage: '/storage', analytics: '/analytics', hub: '/hub', - graphql: '/graphql' + graphql: '/graphql', + ui: '/ui', + workflow: '/workflow', + realtime: '/realtime', + notifications: '/notifications', + ai: '/ai', + i18n: '/i18n', + packages: '/packages', }), // Defaults match standardized spec /** diff --git a/packages/spec/src/api/versioning.test.ts b/packages/spec/src/api/versioning.test.ts new file mode 100644 index 000000000..8f81724d5 --- /dev/null +++ b/packages/spec/src/api/versioning.test.ts @@ -0,0 +1,213 @@ +import { describe, it, expect } from 'vitest'; +import { + VersioningStrategy, + VersionStatus, + VersionDefinitionSchema, + VersioningConfigSchema, + VersionNegotiationResponseSchema, + DEFAULT_VERSIONING_CONFIG, + type VersionDefinition, + type VersioningConfig, + type VersionNegotiationResponse, +} from './versioning.zod'; + +describe('VersioningStrategy', () => { + it('should accept valid strategies', () => { + expect(VersioningStrategy.parse('urlPath')).toBe('urlPath'); + expect(VersioningStrategy.parse('header')).toBe('header'); + expect(VersioningStrategy.parse('queryParam')).toBe('queryParam'); + expect(VersioningStrategy.parse('dateBased')).toBe('dateBased'); + }); + + it('should reject invalid strategies', () => { + expect(() => VersioningStrategy.parse('path')).toThrow(); + expect(() => VersioningStrategy.parse('')).toThrow(); + }); +}); + +describe('VersionStatus', () => { + it('should accept all lifecycle states', () => { + expect(VersionStatus.parse('preview')).toBe('preview'); + expect(VersionStatus.parse('current')).toBe('current'); + expect(VersionStatus.parse('supported')).toBe('supported'); + expect(VersionStatus.parse('deprecated')).toBe('deprecated'); + expect(VersionStatus.parse('retired')).toBe('retired'); + }); + + it('should reject invalid status', () => { + expect(() => VersionStatus.parse('active')).toThrow(); + }); +}); + +describe('VersionDefinitionSchema', () => { + it('should accept a current version', () => { + const version: VersionDefinition = VersionDefinitionSchema.parse({ + version: 'v1', + status: 'current', + releasedAt: '2025-01-15', + description: 'Initial stable release', + }); + + expect(version.version).toBe('v1'); + expect(version.status).toBe('current'); + expect(version.releasedAt).toBe('2025-01-15'); + }); + + it('should accept a deprecated version with sunset info', () => { + const version = VersionDefinitionSchema.parse({ + version: 'v0', + status: 'deprecated', + releasedAt: '2024-06-01', + deprecatedAt: '2025-01-15', + sunsetAt: '2025-07-15', + migrationGuide: 'https://docs.objectstack.dev/migrate/v0-to-v1', + description: 'Legacy API version', + }); + + expect(version.deprecatedAt).toBe('2025-01-15'); + expect(version.sunsetAt).toBe('2025-07-15'); + expect(version.migrationGuide).toContain('migrate'); + }); + + it('should accept a preview version with breaking changes', () => { + const version = VersionDefinitionSchema.parse({ + version: 'v2beta1', + status: 'preview', + releasedAt: '2025-06-01', + breakingChanges: [ + 'Renamed /api/v1/meta to /api/v2/metadata', + 'Changed batch response format', + ], + }); + + expect(version.breakingChanges).toHaveLength(2); + }); + + it('should reject invalid migrationGuide URL', () => { + expect(() => VersionDefinitionSchema.parse({ + version: 'v0', + status: 'deprecated', + releasedAt: '2024-06-01', + migrationGuide: 'not-a-url', + })).toThrow(); + }); +}); + +describe('VersioningConfigSchema', () => { + it('should accept a minimal configuration', () => { + const config: VersioningConfig = VersioningConfigSchema.parse({ + current: 'v1', + default: 'v1', + versions: [ + { version: 'v1', status: 'current', releasedAt: '2025-01-15' }, + ], + }); + + expect(config.strategy).toBe('urlPath'); // default + expect(config.current).toBe('v1'); + expect(config.default).toBe('v1'); + expect(config.headerName).toBe('ObjectStack-Version'); + expect(config.queryParamName).toBe('version'); + expect(config.urlPrefix).toBe('/api'); + expect(config.includeInDiscovery).toBe(true); + }); + + it('should accept a complete configuration', () => { + const config = VersioningConfigSchema.parse({ + strategy: 'header', + current: 'v2', + default: 'v1', + headerName: 'X-API-Version', + versions: [ + { version: 'v1', status: 'supported', releasedAt: '2025-01-15' }, + { version: 'v2', status: 'current', releasedAt: '2025-06-01' }, + { version: 'v3beta', status: 'preview', releasedAt: '2025-12-01' }, + ], + deprecation: { + warnHeader: true, + sunsetHeader: true, + linkHeader: true, + rejectRetired: true, + warningMessage: 'This API version is deprecated. Please upgrade.', + }, + includeInDiscovery: true, + }); + + expect(config.strategy).toBe('header'); + expect(config.headerName).toBe('X-API-Version'); + expect(config.versions).toHaveLength(3); + expect(config.deprecation?.warningMessage).toContain('deprecated'); + }); + + it('should accept date-based versioning', () => { + const config = VersioningConfigSchema.parse({ + strategy: 'dateBased', + current: '2025-06-01', + default: '2025-01-15', + versions: [ + { version: '2025-01-15', status: 'supported', releasedAt: '2025-01-15' }, + { version: '2025-06-01', status: 'current', releasedAt: '2025-06-01' }, + ], + }); + + expect(config.strategy).toBe('dateBased'); + expect(config.current).toBe('2025-06-01'); + }); + + it('should require at least one version', () => { + expect(() => VersioningConfigSchema.parse({ + current: 'v1', + default: 'v1', + versions: [], + })).toThrow(); + }); +}); + +describe('VersionNegotiationResponseSchema', () => { + it('should accept a basic response', () => { + const response: VersionNegotiationResponse = VersionNegotiationResponseSchema.parse({ + current: 'v1', + resolved: 'v1', + supported: ['v1'], + }); + + expect(response.current).toBe('v1'); + expect(response.resolved).toBe('v1'); + expect(response.supported).toContain('v1'); + }); + + it('should accept a full response with deprecated versions', () => { + const response = VersionNegotiationResponseSchema.parse({ + current: 'v2', + requested: 'v1', + resolved: 'v1', + supported: ['v1', 'v2', 'v3beta'], + deprecated: ['v0'], + versions: [ + { version: 'v0', status: 'deprecated', releasedAt: '2024-01-01', deprecatedAt: '2025-01-15', sunsetAt: '2025-07-15' }, + { version: 'v1', status: 'supported', releasedAt: '2025-01-15' }, + { version: 'v2', status: 'current', releasedAt: '2025-06-01' }, + { version: 'v3beta', status: 'preview', releasedAt: '2025-12-01' }, + ], + }); + + expect(response.requested).toBe('v1'); + expect(response.deprecated).toContain('v0'); + expect(response.versions).toHaveLength(4); + }); +}); + +describe('DEFAULT_VERSIONING_CONFIG', () => { + it('should be valid configuration', () => { + const config = VersioningConfigSchema.parse(DEFAULT_VERSIONING_CONFIG); + + expect(config.strategy).toBe('urlPath'); + expect(config.current).toBe('v1'); + expect(config.default).toBe('v1'); + expect(config.versions).toHaveLength(1); + expect(config.versions[0].status).toBe('current'); + expect(config.deprecation?.warnHeader).toBe(true); + expect(config.deprecation?.sunsetHeader).toBe(true); + expect(config.includeInDiscovery).toBe(true); + }); +}); diff --git a/packages/spec/src/api/versioning.zod.ts b/packages/spec/src/api/versioning.zod.ts new file mode 100644 index 000000000..3809d0336 --- /dev/null +++ b/packages/spec/src/api/versioning.zod.ts @@ -0,0 +1,275 @@ +import { z } from 'zod'; + +/** + * # API Versioning Protocol + * + * Defines how API versions are negotiated between client and server. + * Supports multiple versioning strategies and deprecation lifecycle management. + * + * Architecture Alignment: + * - Salesforce: URL path versioning (v57.0, v58.0) + * - Stripe: Date-based versioning (2024-01-01) + * - Kubernetes: API group versioning (v1, v1beta1) + * - GitHub: Accept header versioning (application/vnd.github.v3+json) + * - Microsoft Graph: URL path versioning (v1.0, beta) + */ + +// ========================================== +// Versioning Strategy +// ========================================== + +/** + * API Versioning Strategy + * Determines how the API version is specified by clients. + * + * - `urlPath`: Version in URL path (e.g., /api/v1/data) — Most common, easy to understand + * - `header`: Version in Accept header (e.g., Accept: application/vnd.objectstack.v1+json) + * - `queryParam`: Version in query parameter (e.g., /api/data?version=v1) + * - `dateBased`: Date-based version in header (e.g., ObjectStack-Version: 2025-01-01) — Stripe-style + */ +export const VersioningStrategy = z.enum([ + 'urlPath', + 'header', + 'queryParam', + 'dateBased', +]); + +export type VersioningStrategy = z.infer; + +// ========================================== +// Version Lifecycle +// ========================================== + +/** + * API Version Status + * Lifecycle state of an API version. + * + * - `preview`: Available for testing, may change without notice (e.g., v2beta1) + * - `current`: The recommended stable version + * - `supported`: Older but still maintained (receives security fixes) + * - `deprecated`: Scheduled for removal, clients should migrate + * - `retired`: No longer available, requests return 410 Gone + */ +export const VersionStatus = z.enum([ + 'preview', + 'current', + 'supported', + 'deprecated', + 'retired', +]); + +export type VersionStatus = z.infer; + +// ========================================== +// Version Definition +// ========================================== + +/** + * API Version Definition Schema + * Describes a single API version and its lifecycle metadata. + * + * @example + * { + * "version": "v1", + * "status": "current", + * "releasedAt": "2025-01-15", + * "description": "Initial stable release" + * } + * + * @example Deprecated version + * { + * "version": "v0", + * "status": "deprecated", + * "releasedAt": "2024-06-01", + * "deprecatedAt": "2025-01-15", + * "sunsetAt": "2025-07-15", + * "migrationGuide": "https://docs.objectstack.dev/migrate/v0-to-v1", + * "description": "Legacy API version" + * } + */ +export const VersionDefinitionSchema = z.object({ + /** Version identifier (e.g., "v1", "v2beta1", "2025-01-01") */ + version: z.string().describe('Version identifier (e.g., "v1", "v2beta1", "2025-01-01")'), + + /** Current lifecycle status */ + status: VersionStatus.describe('Lifecycle status of this version'), + + /** Date this version was released (ISO 8601 date) */ + releasedAt: z.string().describe('Release date (ISO 8601, e.g., "2025-01-15")'), + + /** Date this version was deprecated (ISO 8601 date) */ + deprecatedAt: z.string().optional() + .describe('Deprecation date (ISO 8601). Only set for deprecated/retired versions'), + + /** Date this version will be retired (ISO 8601 date) */ + sunsetAt: z.string().optional() + .describe('Sunset date (ISO 8601). After this date, the version returns 410 Gone'), + + /** URL to migration guide for moving to a newer version */ + migrationGuide: z.string().url().optional() + .describe('URL to migration guide for upgrading from this version'), + + /** Human-readable description of this version */ + description: z.string().optional() + .describe('Human-readable description or release notes summary'), + + /** Breaking changes introduced in or since this version */ + breakingChanges: z.array(z.string()).optional() + .describe('List of breaking changes (for preview/new versions)'), +}); + +export type VersionDefinition = z.infer; + +// ========================================== +// Versioning Configuration +// ========================================== + +/** + * API Versioning Configuration Schema + * Complete configuration for API version management. + * + * @example + * { + * "strategy": "urlPath", + * "current": "v1", + * "default": "v1", + * "versions": [ + * { "version": "v1", "status": "current", "releasedAt": "2025-01-15" }, + * { "version": "v2beta1", "status": "preview", "releasedAt": "2025-06-01" } + * ], + * "deprecation": { + * "warnHeader": true, + * "sunsetHeader": true + * } + * } + */ +export const VersioningConfigSchema = z.object({ + /** Versioning strategy */ + strategy: VersioningStrategy.default('urlPath') + .describe('How the API version is specified by clients'), + + /** Current (recommended) API version */ + current: z.string().describe('The current/recommended API version identifier'), + + /** Default version when none specified by client */ + default: z.string().describe('Fallback version when client does not specify one'), + + /** All available API versions */ + versions: z.array(VersionDefinitionSchema) + .min(1) + .describe('All available API versions with lifecycle metadata'), + + /** Header name for header-based versioning */ + headerName: z.string().default('ObjectStack-Version') + .describe('HTTP header name for version negotiation (header/dateBased strategies)'), + + /** Query parameter name for queryParam strategy */ + queryParamName: z.string().default('version') + .describe('Query parameter name for version specification (queryParam strategy)'), + + /** URL prefix pattern for urlPath strategy */ + urlPrefix: z.string().default('/api') + .describe('URL prefix before version segment (urlPath strategy)'), + + /** Deprecation behavior */ + deprecation: z.object({ + /** Include Deprecation header in responses for deprecated versions */ + warnHeader: z.boolean().default(true) + .describe('Include Deprecation header (RFC 8594) in responses'), + + /** Include Sunset header with retirement date */ + sunsetHeader: z.boolean().default(true) + .describe('Include Sunset header (RFC 8594) with retirement date'), + + /** Include Link header pointing to migration guide */ + linkHeader: z.boolean().default(true) + .describe('Include Link header pointing to migration guide URL'), + + /** Whether to reject requests to retired versions */ + rejectRetired: z.boolean().default(true) + .describe('Return 410 Gone for retired API versions'), + + /** Custom deprecation warning message */ + warningMessage: z.string().optional() + .describe('Custom warning message for deprecated version responses'), + }).optional().describe('Deprecation lifecycle behavior'), + + /** Whether to include version info in discovery response */ + includeInDiscovery: z.boolean().default(true) + .describe('Include version information in the API discovery endpoint'), +}); + +export type VersioningConfig = z.infer; +export type VersioningConfigInput = z.input; + +// ========================================== +// Version Negotiation Response +// ========================================== + +/** + * Version Negotiation Response Schema + * Returned when a client requests version information or + * included in the discovery endpoint response. + * + * @example + * { + * "current": "v1", + * "requested": "v1", + * "resolved": "v1", + * "supported": ["v1", "v2beta1"], + * "deprecated": ["v0"], + * "versions": [...] + * } + */ +export const VersionNegotiationResponseSchema = z.object({ + /** The current/recommended version */ + current: z.string().describe('Current recommended API version'), + + /** The version the client requested (if any) */ + requested: z.string().optional().describe('Version requested by the client'), + + /** The version actually being used for this request */ + resolved: z.string().describe('Resolved API version for this request'), + + /** All supported (non-retired) version identifiers */ + supported: z.array(z.string()).describe('All supported version identifiers'), + + /** Deprecated version identifiers (still functional but will be removed) */ + deprecated: z.array(z.string()).optional() + .describe('Deprecated version identifiers'), + + /** Full version definitions (optional, for detailed clients) */ + versions: z.array(VersionDefinitionSchema).optional() + .describe('Full version definitions with lifecycle metadata'), +}); + +export type VersionNegotiationResponse = z.infer; + +// ========================================== +// Default Versioning Configuration +// ========================================== + +/** + * Default versioning configuration for ObjectStack. + * Uses URL path strategy with v1 as the current/default version. + */ +export const DEFAULT_VERSIONING_CONFIG: VersioningConfigInput = { + strategy: 'urlPath', + current: 'v1', + default: 'v1', + versions: [ + { + version: 'v1', + status: 'current', + releasedAt: '2025-01-15', + description: 'ObjectStack API v1 — Initial stable release', + }, + ], + deprecation: { + warnHeader: true, + sunsetHeader: true, + linkHeader: true, + rejectRetired: true, + }, + includeInDiscovery: true, +};