Type conversion completed
This commit is contained in:
parent
3cb49c70ef
commit
ba6aa697a4
175
API_EVOLUTION.md
Normal file
175
API_EVOLUTION.md
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
# API Evolution Detection System
|
||||||
|
|
||||||
|
This system automatically detects when your OpenAPI schema has new endpoints or changed parameters that need to be implemented in the `ApiClient` class.
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### Automatic Detection
|
||||||
|
- **Development Mode**: Automatically runs when `api-client.ts` is imported during development
|
||||||
|
- **Runtime Checking**: Compares available endpoints in the OpenAPI schema with implemented methods
|
||||||
|
- **Console Warnings**: Displays detailed warnings about unimplemented endpoints
|
||||||
|
|
||||||
|
### Schema Comparison
|
||||||
|
- **Hash-based Detection**: Detects when the OpenAPI schema file changes
|
||||||
|
- **Endpoint Analysis**: Identifies new, changed, or unimplemented endpoints
|
||||||
|
- **Parameter Validation**: Suggests checking for parameter changes
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Automatic Checking
|
||||||
|
The system runs automatically in development mode when you import from `api-client.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { apiClient } from './api-client';
|
||||||
|
// Check runs automatically after 1 second delay
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Line Checking
|
||||||
|
You can run API evolution checks from the command line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Full type generation with evolution check
|
||||||
|
./generate-ts-types.sh
|
||||||
|
|
||||||
|
# Quick evolution check only (without regenerating types)
|
||||||
|
./check-api-evolution.sh
|
||||||
|
|
||||||
|
# Or from within the client container
|
||||||
|
npm run check-api-evolution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Checking
|
||||||
|
You can manually trigger checks during development:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { devUtils } from './api-client';
|
||||||
|
|
||||||
|
// Check for API evolution
|
||||||
|
const evolution = await devUtils.checkApiEvolution();
|
||||||
|
|
||||||
|
// Force recheck (bypasses once-per-session limit)
|
||||||
|
devUtils.recheckEndpoints();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Console Output
|
||||||
|
When unimplemented endpoints are found, you'll see:
|
||||||
|
|
||||||
|
**Browser Console (development mode):**
|
||||||
|
```
|
||||||
|
🚨 API Evolution Detection
|
||||||
|
🆕 New API endpoints detected:
|
||||||
|
• GET /ai-voicebot/api/new-feature (get_new_feature_endpoint)
|
||||||
|
⚠️ Unimplemented API endpoints:
|
||||||
|
• POST /ai-voicebot/api/admin/bulk-action
|
||||||
|
💡 Implementation suggestions:
|
||||||
|
Add these methods to ApiClient:
|
||||||
|
async adminBulkAction(): Promise<any> {
|
||||||
|
return this.request<any>('/ai-voicebot/api/admin/bulk-action', { method: 'POST' });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Command Line:**
|
||||||
|
```
|
||||||
|
🔍 API Evolution Check
|
||||||
|
==================================================
|
||||||
|
📊 Summary:
|
||||||
|
Total endpoints: 8
|
||||||
|
Implemented: 7
|
||||||
|
Unimplemented: 1
|
||||||
|
|
||||||
|
⚠️ Unimplemented API endpoints:
|
||||||
|
• POST /ai-voicebot/api/admin/bulk-action
|
||||||
|
Admin bulk action endpoint
|
||||||
|
|
||||||
|
💡 Implementation suggestions:
|
||||||
|
Add these methods to the ApiClient class:
|
||||||
|
|
||||||
|
async adminBulkAction(data?: any): Promise<any> {
|
||||||
|
return this.request<any>('/ai-voicebot/api/admin/bulk-action', { method: 'POST', body: data });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Implemented Endpoints Registry
|
||||||
|
The system maintains a registry of implemented endpoints in `ApiClient`. When you add new methods, update the registry:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In api-evolution-checker.ts
|
||||||
|
private getImplementedEndpoints(): Set<string> {
|
||||||
|
return new Set([
|
||||||
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
// Add new endpoints here:
|
||||||
|
'POST:/ai-voicebot/api/admin/bulk-action',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Schema Location
|
||||||
|
The system attempts to load the OpenAPI schema from:
|
||||||
|
- `/openapi-schema.json` (served by your development server)
|
||||||
|
- Falls back to hardcoded endpoint list if schema file is unavailable
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### When Adding New API Endpoints
|
||||||
|
|
||||||
|
1. **Add endpoint to FastAPI server** (server/main.py)
|
||||||
|
2. **Regenerate types**: Run `./generate-ts-types.sh`
|
||||||
|
3. **Check console** for warnings about unimplemented endpoints
|
||||||
|
4. **Implement methods** in `ApiClient` class
|
||||||
|
5. **Update endpoint registry** in the evolution checker
|
||||||
|
6. **Add convenience methods** to API namespaces if needed
|
||||||
|
|
||||||
|
### Example Implementation
|
||||||
|
|
||||||
|
When you see a warning like:
|
||||||
|
```
|
||||||
|
⚠️ Unimplemented: POST /ai-voicebot/api/admin/bulk-action
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Add the method to `ApiClient`:
|
||||||
|
```typescript
|
||||||
|
async adminBulkAction(data: BulkActionRequest): Promise<BulkActionResponse> {
|
||||||
|
return this.request<BulkActionResponse>('/ai-voicebot/api/admin/bulk-action', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Add to convenience API:
|
||||||
|
```typescript
|
||||||
|
export const adminApi = {
|
||||||
|
listNames: () => apiClient.adminListNames(),
|
||||||
|
setPassword: (data: AdminSetPassword) => apiClient.adminSetPassword(data),
|
||||||
|
clearPassword: (data: AdminClearPassword) => apiClient.adminClearPassword(data),
|
||||||
|
bulkAction: (data: BulkActionRequest) => apiClient.adminBulkAction(data), // New
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Update the registry:
|
||||||
|
```typescript
|
||||||
|
private getImplementedEndpoints(): Set<string> {
|
||||||
|
return new Set([
|
||||||
|
// ... existing endpoints ...
|
||||||
|
'POST:/ai-voicebot/api/admin/bulk-action', // Add this
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
- **Prevents Missing Implementations**: Never forget to implement new API endpoints
|
||||||
|
- **Development Efficiency**: Automatic detection saves time during API evolution
|
||||||
|
- **Type Safety**: Works with generated TypeScript types for full type safety
|
||||||
|
- **Code Generation**: Provides implementation stubs to get started quickly
|
||||||
|
- **Schema Validation**: Detects when OpenAPI schema changes
|
||||||
|
|
||||||
|
## Production Considerations
|
||||||
|
|
||||||
|
- **Development Only**: Evolution checking only runs in development mode
|
||||||
|
- **Performance**: Minimal runtime overhead (single check per session)
|
||||||
|
- **Error Handling**: Gracefully falls back if schema loading fails
|
||||||
|
- **Console Logging**: All output goes to console.warn/info for easy filtering
|
24
README.md
24
README.md
@ -294,4 +294,26 @@ Frontend A Backend Frontend B
|
|||||||
5. **Error Recovery**: Graceful fallbacks when WebRTC negotiation fails
|
5. **Error Recovery**: Graceful fallbacks when WebRTC negotiation fails
|
||||||
6. **Security**: Validate session cookies and sanitize all incoming messages
|
6. **Security**: Validate session cookies and sanitize all incoming messages
|
||||||
|
|
||||||
The backend acts as the signaling server, routing WebRTC negotiation messages between peers while managing application state. Once the P2P connection is established, media flows directly between clients, but the WebSocket connection remains for application-level messaging.
|
The backend acts as the signaling server, routing WebRTC negotiation messages between peers while managing application state. Once the P2P connection is established, media flows directly between clients, but the WebSocket connection remains for application-level messaging.
|
||||||
|
|
||||||
|
## Development Tools
|
||||||
|
|
||||||
|
### TypeScript Type Generation
|
||||||
|
The project includes automatic TypeScript type generation from the FastAPI OpenAPI schema:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Generate types and check for API evolution
|
||||||
|
./generate-ts-types.sh
|
||||||
|
|
||||||
|
# Check API evolution without regenerating types
|
||||||
|
./check-api-evolution.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Evolution Detection
|
||||||
|
The system automatically detects when new API endpoints are added to the server but not implemented in the TypeScript client:
|
||||||
|
|
||||||
|
- **Automatic Detection**: Warnings appear in browser console during development
|
||||||
|
- **Command Line Tools**: Integrated into the type generation workflow
|
||||||
|
- **Implementation Stubs**: Provides ready-to-use code templates for new endpoints
|
||||||
|
|
||||||
|
See `./API_EVOLUTION.md` for detailed documentation.
|
@ -155,3 +155,14 @@ Ensure the generated types are up to date:
|
|||||||
|
|
||||||
### Module Not Found Errors
|
### Module Not Found Errors
|
||||||
Check that the volume mounts are working correctly and files are synced between host and container.
|
Check that the volume mounts are working correctly and files are synced between host and container.
|
||||||
|
|
||||||
|
## API Evolution Detection
|
||||||
|
|
||||||
|
The system now includes automatic detection of API changes:
|
||||||
|
|
||||||
|
- **Automatic Checking**: In development mode, the system automatically warns about unimplemented endpoints
|
||||||
|
- **Console Warnings**: Clear warnings appear in the browser console when new API endpoints are available
|
||||||
|
- **Implementation Stubs**: Provides ready-to-use code stubs for new endpoints
|
||||||
|
- **Schema Monitoring**: Detects when the OpenAPI schema changes
|
||||||
|
|
||||||
|
See `client/src/API_EVOLUTION.md` for detailed documentation on using this feature.
|
||||||
|
31
check-api-evolution.sh
Executable file
31
check-api-evolution.sh
Executable file
@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Standalone API Evolution Check Script
|
||||||
|
# This script only checks for API evolution without regenerating types.
|
||||||
|
# Useful for quick checks during development.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔍 API Evolution Check"
|
||||||
|
echo "======================"
|
||||||
|
|
||||||
|
# Change to the project directory
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Check if schema file exists
|
||||||
|
if [ ! -f "client/openapi-schema.json" ]; then
|
||||||
|
echo "❌ OpenAPI schema not found. Run ./generate-ts-types.sh first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the frontend container is running
|
||||||
|
if ! docker compose ps static-frontend | grep -q "Up"; then
|
||||||
|
echo "📋 Starting frontend container..."
|
||||||
|
docker compose up -d static-frontend
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📋 Running API evolution check..."
|
||||||
|
docker compose exec static-frontend node check-api-evolution.js
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "💡 To regenerate types and schema: ./generate-ts-types.sh"
|
||||||
|
echo "💡 To run this check only: ./check-api-evolution.sh"
|
180
client/check-api-evolution.js
Normal file
180
client/check-api-evolution.js
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Command-line API evolution checker
|
||||||
|
* This script runs the API evolution detection system from the command line
|
||||||
|
* and provides detailed output about missing or changed endpoints.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Simple implementation of the evolution checker for Node.js
|
||||||
|
class CommandLineEvolutionChecker {
|
||||||
|
constructor() {
|
||||||
|
this.schemaPath = path.join(__dirname, 'openapi-schema.json');
|
||||||
|
this.implementedEndpoints = new Set([
|
||||||
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
'POST:/ai-voicebot/api/admin/clear_password',
|
||||||
|
'GET:/ai-voicebot/api/health',
|
||||||
|
'GET:/ai-voicebot/api/session',
|
||||||
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSchema() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.schemaPath)) {
|
||||||
|
throw new Error(`Schema file not found at: ${this.schemaPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaContent = fs.readFileSync(this.schemaPath, 'utf8');
|
||||||
|
return JSON.parse(schemaContent);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to load OpenAPI schema: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractEndpoints(schema) {
|
||||||
|
const endpoints = [];
|
||||||
|
|
||||||
|
if (!schema.paths) {
|
||||||
|
throw new Error('No paths found in OpenAPI schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(schema.paths).forEach(path => {
|
||||||
|
// Skip the generic proxy endpoint
|
||||||
|
if (path === '/ai-voicebot/{path}') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathObj = schema.paths[path];
|
||||||
|
Object.keys(pathObj).forEach(method => {
|
||||||
|
const operation = pathObj[method];
|
||||||
|
const endpointKey = `${method.toUpperCase()}:${path}`;
|
||||||
|
|
||||||
|
endpoints.push({
|
||||||
|
path,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
operationId: operation.operationId,
|
||||||
|
summary: operation.summary,
|
||||||
|
implemented: this.implementedEndpoints.has(endpointKey),
|
||||||
|
endpointKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateMethodName(endpoint) {
|
||||||
|
if (endpoint.operationId) {
|
||||||
|
// Convert snake_case operation ID to camelCase
|
||||||
|
return endpoint.operationId.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: generate from path and method
|
||||||
|
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
||||||
|
const lastPart = pathParts[pathParts.length - 1] || 'endpoint';
|
||||||
|
const method = endpoint.method.toLowerCase();
|
||||||
|
|
||||||
|
if (method === 'get') {
|
||||||
|
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
} else {
|
||||||
|
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateImplementationStub(endpoint) {
|
||||||
|
const methodName = this.generateMethodName(endpoint);
|
||||||
|
const hasPathParams = endpoint.path.includes('{');
|
||||||
|
const needsBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
||||||
|
|
||||||
|
let params = [];
|
||||||
|
let requestOptions = `{ method: '${endpoint.method}'`;
|
||||||
|
|
||||||
|
// Add path parameters
|
||||||
|
if (hasPathParams) {
|
||||||
|
const pathParams = endpoint.path.match(/\{([^}]+)\}/g) || [];
|
||||||
|
pathParams.forEach(param => {
|
||||||
|
const paramName = param.replace(/[{}]/g, '');
|
||||||
|
params.push(`${paramName}: string`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body parameter for methods that typically need one
|
||||||
|
if (needsBody) {
|
||||||
|
params.push('data?: any');
|
||||||
|
requestOptions += ', body: data';
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions += ' }';
|
||||||
|
|
||||||
|
const paramString = params.length > 0 ? params.join(', ') : '';
|
||||||
|
|
||||||
|
return ` async ${methodName}(${paramString}): Promise<any> {
|
||||||
|
return this.request<any>('${endpoint.path}', ${requestOptions});
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runCheck() {
|
||||||
|
console.log('🔍 API Evolution Check');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = this.loadSchema();
|
||||||
|
const endpoints = this.extractEndpoints(schema);
|
||||||
|
|
||||||
|
const implemented = endpoints.filter(ep => ep.implemented);
|
||||||
|
const unimplemented = endpoints.filter(ep => !ep.implemented);
|
||||||
|
|
||||||
|
console.log(`📊 Summary:`);
|
||||||
|
console.log(` Total endpoints: ${endpoints.length}`);
|
||||||
|
console.log(` Implemented: ${implemented.length}`);
|
||||||
|
console.log(` Unimplemented: ${unimplemented.length}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (unimplemented.length === 0) {
|
||||||
|
console.log('✅ All API endpoints are implemented in ApiClient!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⚠️ Unimplemented API endpoints:');
|
||||||
|
unimplemented.forEach(endpoint => {
|
||||||
|
console.log(` • ${endpoint.method} ${endpoint.path}`);
|
||||||
|
if (endpoint.summary) {
|
||||||
|
console.log(` ${endpoint.summary}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('💡 Implementation suggestions:');
|
||||||
|
console.log('Add these methods to the ApiClient class:');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
unimplemented.forEach(endpoint => {
|
||||||
|
console.log(this.generateImplementationStub(endpoint));
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 Next steps:');
|
||||||
|
console.log('1. Copy the suggested methods above into client/src/api-client.ts');
|
||||||
|
console.log('2. Update the implementedEndpoints Set in api-evolution-checker.ts');
|
||||||
|
console.log('3. Add convenience methods to the appropriate API namespaces');
|
||||||
|
console.log('4. Update type imports if needed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during API evolution check:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the check if this script is executed directly
|
||||||
|
if (require.main === module) {
|
||||||
|
const checker = new CommandLineEvolutionChecker();
|
||||||
|
checker.runCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { CommandLineEvolutionChecker };
|
177
client/demo-api-evolution.js
Normal file
177
client/demo-api-evolution.js
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Demo script to show API evolution detection in action
|
||||||
|
* This simulates having a missing endpoint to demonstrate the output
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class DemoEvolutionChecker {
|
||||||
|
constructor() {
|
||||||
|
this.schemaPath = path.join(__dirname, 'openapi-schema.json');
|
||||||
|
// Simulate missing the 'clear_password' endpoint to show detection
|
||||||
|
this.implementedEndpoints = new Set([
|
||||||
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
// 'POST:/ai-voicebot/api/admin/clear_password', // Commented out to simulate missing
|
||||||
|
'GET:/ai-voicebot/api/health',
|
||||||
|
'GET:/ai-voicebot/api/session',
|
||||||
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSchema() {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(this.schemaPath)) {
|
||||||
|
throw new Error(`Schema file not found at: ${this.schemaPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaContent = fs.readFileSync(this.schemaPath, 'utf8');
|
||||||
|
return JSON.parse(schemaContent);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to load OpenAPI schema: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractEndpoints(schema) {
|
||||||
|
const endpoints = [];
|
||||||
|
|
||||||
|
if (!schema.paths) {
|
||||||
|
throw new Error('No paths found in OpenAPI schema');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(schema.paths).forEach(path => {
|
||||||
|
// Skip the generic proxy endpoint
|
||||||
|
if (path === '/ai-voicebot/{path}') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathObj = schema.paths[path];
|
||||||
|
Object.keys(pathObj).forEach(method => {
|
||||||
|
const operation = pathObj[method];
|
||||||
|
const endpointKey = `${method.toUpperCase()}:${path}`;
|
||||||
|
|
||||||
|
endpoints.push({
|
||||||
|
path,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
operationId: operation.operationId,
|
||||||
|
summary: operation.summary,
|
||||||
|
implemented: this.implementedEndpoints.has(endpointKey),
|
||||||
|
endpointKey
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateMethodName(endpoint) {
|
||||||
|
if (endpoint.operationId) {
|
||||||
|
// Convert snake_case operation ID to camelCase
|
||||||
|
return endpoint.operationId.replace(/_([a-z])/g, (match, letter) => letter.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: generate from path and method
|
||||||
|
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
||||||
|
const lastPart = pathParts[pathParts.length - 1] || 'endpoint';
|
||||||
|
const method = endpoint.method.toLowerCase();
|
||||||
|
|
||||||
|
if (method === 'get') {
|
||||||
|
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
} else {
|
||||||
|
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
generateImplementationStub(endpoint) {
|
||||||
|
const methodName = this.generateMethodName(endpoint);
|
||||||
|
const hasPathParams = endpoint.path.includes('{');
|
||||||
|
const needsBody = ['POST', 'PUT', 'PATCH'].includes(endpoint.method);
|
||||||
|
|
||||||
|
let params = [];
|
||||||
|
let requestOptions = `{ method: '${endpoint.method}'`;
|
||||||
|
|
||||||
|
// Add path parameters
|
||||||
|
if (hasPathParams) {
|
||||||
|
const pathParams = endpoint.path.match(/\{([^}]+)\}/g) || [];
|
||||||
|
pathParams.forEach(param => {
|
||||||
|
const paramName = param.replace(/[{}]/g, '');
|
||||||
|
params.push(`${paramName}: string`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add body parameter for methods that typically need one
|
||||||
|
if (needsBody) {
|
||||||
|
params.push('data?: any');
|
||||||
|
requestOptions += ', body: data';
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions += ' }';
|
||||||
|
|
||||||
|
const paramString = params.length > 0 ? params.join(', ') : '';
|
||||||
|
|
||||||
|
return ` async ${methodName}(${paramString}): Promise<any> {
|
||||||
|
return this.request<any>('${endpoint.path}', ${requestOptions});
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async runDemo() {
|
||||||
|
console.log('🔍 API Evolution Check (Demo Mode)');
|
||||||
|
console.log('='.repeat(50));
|
||||||
|
console.log('Note: This demo simulates a missing endpoint to show detection in action');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schema = this.loadSchema();
|
||||||
|
const endpoints = this.extractEndpoints(schema);
|
||||||
|
|
||||||
|
const implemented = endpoints.filter(ep => ep.implemented);
|
||||||
|
const unimplemented = endpoints.filter(ep => !ep.implemented);
|
||||||
|
|
||||||
|
console.log(`📊 Summary:`);
|
||||||
|
console.log(` Total endpoints: ${endpoints.length}`);
|
||||||
|
console.log(` Implemented: ${implemented.length}`);
|
||||||
|
console.log(` Unimplemented: ${unimplemented.length}`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
if (unimplemented.length === 0) {
|
||||||
|
console.log('✅ All API endpoints are implemented in ApiClient!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⚠️ Unimplemented API endpoints:');
|
||||||
|
unimplemented.forEach(endpoint => {
|
||||||
|
console.log(` • ${endpoint.method} ${endpoint.path}`);
|
||||||
|
if (endpoint.summary) {
|
||||||
|
console.log(` ${endpoint.summary}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('');
|
||||||
|
console.log('💡 Implementation suggestions:');
|
||||||
|
console.log('Add these methods to the ApiClient class:');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
unimplemented.forEach(endpoint => {
|
||||||
|
console.log(this.generateImplementationStub(endpoint));
|
||||||
|
console.log('');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('📝 Next steps:');
|
||||||
|
console.log('1. Copy the suggested methods above into client/src/api-client.ts');
|
||||||
|
console.log('2. Update the implementedEndpoints Set in the evolution checker');
|
||||||
|
console.log('3. Add convenience methods to the appropriate API namespaces');
|
||||||
|
console.log('4. Update type imports if needed');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Error during API evolution check:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the demo
|
||||||
|
const checker = new DemoEvolutionChecker();
|
||||||
|
checker.runDemo();
|
@ -38,6 +38,7 @@
|
|||||||
"generate-schema": "cd ../server && uv run python3 generate_schema_simple.py",
|
"generate-schema": "cd ../server && uv run python3 generate_schema_simple.py",
|
||||||
"generate-types": "npx openapi-typescript openapi-schema.json -o src/api-types.ts",
|
"generate-types": "npx openapi-typescript openapi-schema.json -o src/api-types.ts",
|
||||||
"generate-api-types": "npm run generate-schema && npm run generate-types",
|
"generate-api-types": "npm run generate-schema && npm run generate-types",
|
||||||
|
"check-api-evolution": "node check-api-evolution.js",
|
||||||
"prebuild": "npm run generate-api-types"
|
"prebuild": "npm run generate-api-types"
|
||||||
},
|
},
|
||||||
"npmConfig": {
|
"npmConfig": {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// TypeScript API client for AI Voicebot server
|
// TypeScript API client for AI Voicebot server
|
||||||
import { components } from './api-types';
|
import { components, paths } from "./api-types";
|
||||||
|
|
||||||
// Re-export commonly used types from the generated schema
|
// Re-export commonly used types from the generated schema
|
||||||
export type LobbyModel = components['schemas']['LobbyModel'];
|
export type LobbyModel = components['schemas']['LobbyModel'];
|
||||||
@ -116,6 +116,186 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// API Evolution Detection System
|
||||||
|
interface ApiEndpoint {
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
implemented: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ApiEvolutionChecker {
|
||||||
|
private static instance: ApiEvolutionChecker;
|
||||||
|
private checkedOnce = false;
|
||||||
|
|
||||||
|
static getInstance(): ApiEvolutionChecker {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new ApiEvolutionChecker();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImplementedEndpoints(): Set<string> {
|
||||||
|
// Define all endpoints that are currently implemented in ApiClient
|
||||||
|
return new Set([
|
||||||
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
'POST:/ai-voicebot/api/admin/clear_password',
|
||||||
|
'GET:/ai-voicebot/api/health',
|
||||||
|
'GET:/ai-voicebot/api/session',
|
||||||
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getAvailableEndpoints(): ApiEndpoint[] {
|
||||||
|
// Extract all endpoints from the generated paths type
|
||||||
|
const endpoints: ApiEndpoint[] = [];
|
||||||
|
const implementedSet = this.getImplementedEndpoints();
|
||||||
|
|
||||||
|
// Type-safe extraction of paths from the generated schema
|
||||||
|
const pathKeys = [
|
||||||
|
'/ai-voicebot/api/admin/names',
|
||||||
|
'/ai-voicebot/api/admin/set_password',
|
||||||
|
'/ai-voicebot/api/admin/clear_password',
|
||||||
|
'/ai-voicebot/api/health',
|
||||||
|
'/ai-voicebot/api/session',
|
||||||
|
'/ai-voicebot/api/lobby',
|
||||||
|
'/ai-voicebot/api/lobby/{session_id}',
|
||||||
|
'/ai-voicebot/{path}' // Generic catch-all, we'll skip this one
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
pathKeys.forEach(path => {
|
||||||
|
if (path === '/ai-voicebot/{path}') {
|
||||||
|
// Skip the generic proxy endpoint
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each HTTP method that might be available for this path
|
||||||
|
const possibleMethods = ['get', 'post', 'put', 'delete', 'patch'] as const;
|
||||||
|
|
||||||
|
possibleMethods.forEach(method => {
|
||||||
|
const endpointKey = `${method.toUpperCase()}:${path}`;
|
||||||
|
const implemented = implementedSet.has(endpointKey);
|
||||||
|
|
||||||
|
// We can't directly check if the method exists at runtime due to TypeScript compilation,
|
||||||
|
// but we know from our grep search what exists. Let's be more explicit:
|
||||||
|
const knownEndpoints: Record<string, string[]> = {
|
||||||
|
'/ai-voicebot/api/admin/names': ['GET'],
|
||||||
|
'/ai-voicebot/api/admin/set_password': ['POST'],
|
||||||
|
'/ai-voicebot/api/admin/clear_password': ['POST'],
|
||||||
|
'/ai-voicebot/api/health': ['GET'],
|
||||||
|
'/ai-voicebot/api/session': ['GET'],
|
||||||
|
'/ai-voicebot/api/lobby': ['GET'],
|
||||||
|
'/ai-voicebot/api/lobby/{session_id}': ['POST']
|
||||||
|
};
|
||||||
|
|
||||||
|
const pathMethods = knownEndpoints[path];
|
||||||
|
if (pathMethods && pathMethods.includes(method.toUpperCase())) {
|
||||||
|
endpoints.push({
|
||||||
|
path,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
implemented
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkForNewEndpoints(): void {
|
||||||
|
if (this.checkedOnce) {
|
||||||
|
return; // Only check once per session to avoid spam
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkedOnce = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoints = this.getAvailableEndpoints();
|
||||||
|
const unimplemented = endpoints.filter(ep => !ep.implemented);
|
||||||
|
|
||||||
|
if (unimplemented.length > 0) {
|
||||||
|
console.group('🚨 API Evolution Warning');
|
||||||
|
console.warn(
|
||||||
|
`Found ${unimplemented.length} API endpoints that are not implemented in ApiClient:`
|
||||||
|
);
|
||||||
|
|
||||||
|
unimplemented.forEach(endpoint => {
|
||||||
|
console.warn(` • ${endpoint.method} ${endpoint.path}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.warn('Consider updating api-client.ts to implement these endpoints.');
|
||||||
|
console.warn('Available types can be found in api-types.ts');
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
|
console.info('✅ All available API endpoints are implemented in ApiClient');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for potential parameter changes by looking at method signatures
|
||||||
|
this.checkForParameterChanges();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to check for API evolution:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkForParameterChanges(): void {
|
||||||
|
// This is a simpler check - we could extend this to compare parameter schemas
|
||||||
|
// For now, we'll just remind developers to check if their implementation matches the schema
|
||||||
|
console.info('💡 Tip: Regularly compare your method implementations with the generated types in api-types.ts to ensure parameter compatibility');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to manually trigger a fresh check (useful for development)
|
||||||
|
recheckEndpoints(): void {
|
||||||
|
this.checkedOnce = false;
|
||||||
|
this.checkForNewEndpoints();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the checker for manual use if needed
|
||||||
|
export const apiEvolutionChecker = ApiEvolutionChecker.getInstance();
|
||||||
|
|
||||||
|
// Utility functions for development
|
||||||
|
export const devUtils = {
|
||||||
|
/**
|
||||||
|
* Manually check for API evolution and log results
|
||||||
|
*/
|
||||||
|
async checkApiEvolution() {
|
||||||
|
if (process.env.NODE_ENV !== 'development') {
|
||||||
|
console.warn('API evolution checking is only available in development mode');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { advancedApiChecker } = await import('./api-evolution-checker');
|
||||||
|
const evolution = await advancedApiChecker.checkSchemaEvolution();
|
||||||
|
|
||||||
|
console.group('🔍 Manual API Evolution Check');
|
||||||
|
console.log('Unimplemented endpoints:', evolution.unimplementedEndpoints.length);
|
||||||
|
console.log('New endpoints:', evolution.newEndpoints.length);
|
||||||
|
console.log('Schema changed:', evolution.hasChangedEndpoints);
|
||||||
|
|
||||||
|
if (evolution.unimplementedEndpoints.length > 0) {
|
||||||
|
console.log('\nImplementation stubs:');
|
||||||
|
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
return evolution;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check API evolution:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force a recheck of endpoints (bypasses the "once per session" limitation)
|
||||||
|
*/
|
||||||
|
recheckEndpoints() {
|
||||||
|
apiEvolutionChecker.recheckEndpoints();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Default client instance
|
// Default client instance
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|
||||||
@ -132,3 +312,52 @@ export const sessionsApi = {
|
|||||||
getCurrent: () => apiClient.getSession(),
|
getCurrent: () => apiClient.getSession(),
|
||||||
createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data),
|
createLobby: (sessionId: string, data: LobbyCreateRequest) => apiClient.createLobby(sessionId, data),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Automatically check for API evolution when this module is loaded
|
||||||
|
// This will warn developers if new endpoints are available but not implemented
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
// Import the advanced checker dynamically to avoid circular dependencies
|
||||||
|
import('./api-evolution-checker').then(({ advancedApiChecker }) => {
|
||||||
|
// Run the check after a short delay to ensure all modules are loaded
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const evolution = await advancedApiChecker.checkSchemaEvolution();
|
||||||
|
|
||||||
|
if (evolution.unimplementedEndpoints.length > 0 || evolution.hasNewEndpoints) {
|
||||||
|
console.group('🚨 API Evolution Detection');
|
||||||
|
|
||||||
|
if (evolution.hasNewEndpoints && evolution.newEndpoints.length > 0) {
|
||||||
|
console.warn('🆕 New API endpoints detected:');
|
||||||
|
evolution.newEndpoints.forEach(ep => {
|
||||||
|
console.warn(` • ${ep.method} ${ep.path} (${ep.operationId})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evolution.unimplementedEndpoints.length > 0) {
|
||||||
|
console.warn('⚠️ Unimplemented API endpoints:');
|
||||||
|
evolution.unimplementedEndpoints.forEach(ep => {
|
||||||
|
console.warn(` • ${ep.method} ${ep.path}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evolution.hasChangedEndpoints) {
|
||||||
|
console.warn('🔄 API schema has changed - check for parameter updates');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupCollapsed('💡 Implementation suggestions:');
|
||||||
|
console.log('Add these methods to ApiClient:');
|
||||||
|
console.log(advancedApiChecker.generateImplementationStubs(evolution.unimplementedEndpoints));
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
} else {
|
||||||
|
console.info('✅ All API endpoints are implemented');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('API evolution check failed:', error);
|
||||||
|
// Fallback to simple checker
|
||||||
|
apiEvolutionChecker.checkForNewEndpoints();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
203
client/src/api-evolution-checker.ts
Normal file
203
client/src/api-evolution-checker.ts
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
// Advanced API Evolution Detection utilities
|
||||||
|
// This module provides runtime introspection of OpenAPI schema changes
|
||||||
|
|
||||||
|
import { paths } from './api-types';
|
||||||
|
|
||||||
|
export interface EndpointInfo {
|
||||||
|
path: string;
|
||||||
|
method: string;
|
||||||
|
operationId?: string;
|
||||||
|
implemented: boolean;
|
||||||
|
hasParameterChanges?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AdvancedApiEvolutionChecker {
|
||||||
|
private static instance: AdvancedApiEvolutionChecker;
|
||||||
|
private lastSchemaHash: string | null = null;
|
||||||
|
|
||||||
|
static getInstance(): AdvancedApiEvolutionChecker {
|
||||||
|
if (!this.instance) {
|
||||||
|
this.instance = new AdvancedApiEvolutionChecker();
|
||||||
|
}
|
||||||
|
return this.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all endpoints from the OpenAPI schema by analyzing the paths type
|
||||||
|
*/
|
||||||
|
private extractEndpointsFromSchema(): EndpointInfo[] {
|
||||||
|
const endpoints: EndpointInfo[] = [];
|
||||||
|
|
||||||
|
// In a real implementation, we would need to parse the actual OpenAPI JSON
|
||||||
|
// since TypeScript types are erased at runtime. For now, we'll maintain
|
||||||
|
// a list that should be updated when new endpoints are added to the schema.
|
||||||
|
const knownSchemaEndpoints = [
|
||||||
|
{ path: '/ai-voicebot/api/admin/names', method: 'GET', operationId: 'admin_list_names_ai_voicebot_api_admin_names_get' },
|
||||||
|
{ path: '/ai-voicebot/api/admin/set_password', method: 'POST', operationId: 'admin_set_password_ai_voicebot_api_admin_set_password_post' },
|
||||||
|
{ path: '/ai-voicebot/api/admin/clear_password', method: 'POST', operationId: 'admin_clear_password_ai_voicebot_api_admin_clear_password_post' },
|
||||||
|
{ path: '/ai-voicebot/api/health', method: 'GET', operationId: 'health_ai_voicebot_api_health_get' },
|
||||||
|
{ path: '/ai-voicebot/api/session', method: 'GET', operationId: 'session_ai_voicebot_api_session_get' },
|
||||||
|
{ path: '/ai-voicebot/api/lobby', method: 'GET', operationId: 'get_lobbies_ai_voicebot_api_lobby_get' },
|
||||||
|
{ path: '/ai-voicebot/api/lobby/{session_id}', method: 'POST', operationId: 'lobby_create_ai_voicebot_api_lobby__session_id__post' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get implemented endpoints from ApiClient
|
||||||
|
const implementedEndpoints = this.getImplementedEndpoints();
|
||||||
|
|
||||||
|
knownSchemaEndpoints.forEach(endpoint => {
|
||||||
|
const key = `${endpoint.method}:${endpoint.path}`;
|
||||||
|
endpoints.push({
|
||||||
|
...endpoint,
|
||||||
|
implemented: implementedEndpoints.has(key)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getImplementedEndpoints(): Set<string> {
|
||||||
|
return new Set([
|
||||||
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
'POST:/ai-voicebot/api/admin/clear_password',
|
||||||
|
'GET:/ai-voicebot/api/health',
|
||||||
|
'GET:/ai-voicebot/api/session',
|
||||||
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the OpenAPI schema from the JSON file to detect runtime changes
|
||||||
|
*/
|
||||||
|
async loadSchemaFromJson(): Promise<any> {
|
||||||
|
try {
|
||||||
|
// In a real implementation, you might fetch this from a URL or import it
|
||||||
|
const response = await fetch('/openapi-schema.json');
|
||||||
|
if (response.ok) {
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Could not load OpenAPI schema for evolution checking:', error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare current schema with a previous version to detect changes
|
||||||
|
*/
|
||||||
|
async checkSchemaEvolution(): Promise<{
|
||||||
|
hasNewEndpoints: boolean;
|
||||||
|
hasChangedEndpoints: boolean;
|
||||||
|
newEndpoints: EndpointInfo[];
|
||||||
|
changedEndpoints: EndpointInfo[];
|
||||||
|
unimplementedEndpoints: EndpointInfo[];
|
||||||
|
}> {
|
||||||
|
const currentEndpoints = this.extractEndpointsFromSchema();
|
||||||
|
const unimplementedEndpoints = currentEndpoints.filter(ep => !ep.implemented);
|
||||||
|
|
||||||
|
// Try to load the actual schema for more detailed analysis
|
||||||
|
const schema = await this.loadSchemaFromJson();
|
||||||
|
let hasNewEndpoints = false;
|
||||||
|
let hasChangedEndpoints = false;
|
||||||
|
let newEndpoints: EndpointInfo[] = [];
|
||||||
|
let changedEndpoints: EndpointInfo[] = [];
|
||||||
|
|
||||||
|
if (schema) {
|
||||||
|
// Calculate a simple hash of the schema to detect changes
|
||||||
|
const schemaString = JSON.stringify(schema.paths || {});
|
||||||
|
const currentHash = this.simpleHash(schemaString);
|
||||||
|
|
||||||
|
if (this.lastSchemaHash && this.lastSchemaHash !== currentHash) {
|
||||||
|
hasChangedEndpoints = true;
|
||||||
|
console.info('🔄 OpenAPI schema has changed since last check');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastSchemaHash = currentHash;
|
||||||
|
|
||||||
|
// Extract endpoints from the actual schema
|
||||||
|
if (schema.paths) {
|
||||||
|
Object.keys(schema.paths).forEach(path => {
|
||||||
|
if (path === '/ai-voicebot/{path}') return; // Skip generic proxy
|
||||||
|
|
||||||
|
const pathObj = schema.paths[path];
|
||||||
|
Object.keys(pathObj).forEach(method => {
|
||||||
|
const endpoint = `${method.toUpperCase()}:${path}`;
|
||||||
|
const implementedEndpoints = this.getImplementedEndpoints();
|
||||||
|
|
||||||
|
if (!implementedEndpoints.has(endpoint)) {
|
||||||
|
const endpointInfo: EndpointInfo = {
|
||||||
|
path,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
operationId: pathObj[method].operationId,
|
||||||
|
implemented: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if this is a new endpoint (not in our known list)
|
||||||
|
const isKnown = currentEndpoints.some(ep =>
|
||||||
|
ep.path === path && ep.method === method.toUpperCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isKnown) {
|
||||||
|
hasNewEndpoints = true;
|
||||||
|
newEndpoints.push(endpointInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasNewEndpoints,
|
||||||
|
hasChangedEndpoints,
|
||||||
|
newEndpoints,
|
||||||
|
changedEndpoints,
|
||||||
|
unimplementedEndpoints
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private simpleHash(str: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < str.length; i++) {
|
||||||
|
const char = str.charCodeAt(i);
|
||||||
|
hash = ((hash << 5) - hash) + char;
|
||||||
|
hash = hash & hash; // Convert to 32bit integer
|
||||||
|
}
|
||||||
|
return hash.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate implementation stubs for unimplemented endpoints
|
||||||
|
*/
|
||||||
|
generateImplementationStubs(endpoints: EndpointInfo[]): string {
|
||||||
|
return endpoints.map(endpoint => {
|
||||||
|
const methodName = this.generateMethodName(endpoint);
|
||||||
|
const returnType = `Promise<any>`; // Could be more specific with schema analysis
|
||||||
|
|
||||||
|
return ` async ${methodName}(): ${returnType} {
|
||||||
|
return this.request<any>('${endpoint.path}', { method: '${endpoint.method}' });
|
||||||
|
}`;
|
||||||
|
}).join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMethodName(endpoint: EndpointInfo): string {
|
||||||
|
// Convert operation ID or path to camelCase method name
|
||||||
|
if (endpoint.operationId) {
|
||||||
|
return endpoint.operationId.replace(/_[a-z]/g, (match) => match[1].toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: generate from path and method
|
||||||
|
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
||||||
|
const lastPart = pathParts[pathParts.length - 1];
|
||||||
|
const method = endpoint.method.toLowerCase();
|
||||||
|
|
||||||
|
if (method === 'get') {
|
||||||
|
return `get${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
} else {
|
||||||
|
return `${method}${lastPart.charAt(0).toUpperCase() + lastPart.slice(1)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const advancedApiChecker = AdvancedApiEvolutionChecker.getInstance();
|
@ -27,10 +27,15 @@ docker compose exec static-frontend npx openapi-typescript openapi-schema.json -
|
|||||||
echo "📋 Step 5: Running TypeScript type checking..."
|
echo "📋 Step 5: Running TypeScript type checking..."
|
||||||
docker compose exec static-frontend npm run type-check
|
docker compose exec static-frontend npm run type-check
|
||||||
|
|
||||||
|
echo "📋 Step 6: Running API evolution check..."
|
||||||
|
docker compose exec static-frontend node check-api-evolution.js
|
||||||
|
|
||||||
echo "✅ TypeScript generation complete!"
|
echo "✅ TypeScript generation complete!"
|
||||||
echo "📄 Generated files:"
|
echo "📄 Generated files:"
|
||||||
echo " - client/openapi-schema.json (OpenAPI schema)"
|
echo " - client/openapi-schema.json (OpenAPI schema)"
|
||||||
echo " - client/src/api-types.ts (TypeScript types)"
|
echo " - client/src/api-types.ts (TypeScript types)"
|
||||||
|
echo ""
|
||||||
|
echo "📄 Manual files (uses generated types):"
|
||||||
echo " - client/src/api-client.ts (API client utilities)"
|
echo " - client/src/api-client.ts (API client utilities)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "💡 Usage in your React components:"
|
echo "💡 Usage in your React components:"
|
||||||
|
@ -140,7 +140,19 @@ class WebSocketMessageModel(BaseModel):
|
|||||||
"""Base model for all WebSocket messages"""
|
"""Base model for all WebSocket messages"""
|
||||||
|
|
||||||
type: str
|
type: str
|
||||||
data: JoinStatusModel | UserJoinedModel | LobbyStateModel | UpdateNameModel
|
data: (
|
||||||
|
JoinStatusModel
|
||||||
|
| UserJoinedModel
|
||||||
|
| LobbyStateModel
|
||||||
|
| UpdateNameModel
|
||||||
|
| ICECandidateDictModel
|
||||||
|
| AddPeerModel
|
||||||
|
| RemovePeerModel
|
||||||
|
| SessionDescriptionModel
|
||||||
|
| IceCandidateModel
|
||||||
|
| LobbyCreateResponse
|
||||||
|
| Dict[str, str]
|
||||||
|
) # Generic dict for simple messages
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
@ -21,7 +21,7 @@ export PATH="$VIRTUAL_ENV/bin:$PATH"
|
|||||||
# Launch voicebot in production or development mode
|
# Launch voicebot in production or development mode
|
||||||
if [ "$PRODUCTION" != "true" ]; then
|
if [ "$PRODUCTION" != "true" ]; then
|
||||||
echo "Starting voicebot in development mode with auto-reload..."
|
echo "Starting voicebot in development mode with auto-reload..."
|
||||||
python3 scripts/reload_runner.py --watch . -- uv run main.py \
|
python3 scripts/reload_runner.py --watch ../voicebot --watch ../shared -- uv run main.py \
|
||||||
--insecure \
|
--insecure \
|
||||||
--server-url https://ketrenos.com/ai-voicebot \
|
--server-url https://ketrenos.com/ai-voicebot \
|
||||||
--lobby default \
|
--lobby default \
|
||||||
|
Loading…
x
Reference in New Issue
Block a user