Type generation
This commit is contained in:
parent
3a72f6097e
commit
d940c2cbef
238
AUTOMATED_API_CLIENT.md
Normal file
238
AUTOMATED_API_CLIENT.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# Automated API Client Generation System
|
||||||
|
|
||||||
|
This document explains the automated TypeScript API client generation and update system for the AI Voicebot project.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The system automatically:
|
||||||
|
1. **Generates OpenAPI schema** from FastAPI server
|
||||||
|
2. **Creates TypeScript types** from the schema
|
||||||
|
3. **Updates API client** with missing endpoint implementations using dynamic paths
|
||||||
|
4. **Updates evolution checker** with current endpoint lists
|
||||||
|
5. **Validates TypeScript** compilation
|
||||||
|
6. **Runs evolution checks** to ensure completeness
|
||||||
|
|
||||||
|
All generated API calls use the `PUBLIC_URL` environment variable to dynamically construct paths, making the system deployable to any base path without hardcoded `/ai-voicebot` prefixes.
|
||||||
|
|
||||||
|
## Files in the System
|
||||||
|
|
||||||
|
### Generated Files (Auto-updated)
|
||||||
|
- `client/openapi-schema.json` - OpenAPI schema from server
|
||||||
|
- `client/src/api-types.ts` - TypeScript type definitions
|
||||||
|
- `client/src/api-client.ts` - API client (auto-sections updated)
|
||||||
|
- `client/src/api-evolution-checker.ts` - Evolution checker (lists updated)
|
||||||
|
|
||||||
|
### Manual Files
|
||||||
|
- `generate-ts-types.sh` - Main orchestration script
|
||||||
|
- `client/update-api-client.js` - API client updater utility
|
||||||
|
- `client/src/api-usage-examples.ts` - Usage examples and patterns
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
The system uses environment variables for dynamic path configuration:
|
||||||
|
|
||||||
|
- **`PUBLIC_URL`** - Base path for the application (e.g., `/ai-voicebot`, `/my-app`, etc.)
|
||||||
|
- Used in: API paths, schema loading, asset paths
|
||||||
|
- Default: `""` (empty string for root deployment)
|
||||||
|
- Set in: Docker environment, build process, or runtime
|
||||||
|
|
||||||
|
### Dynamic Path Handling
|
||||||
|
|
||||||
|
All API endpoints use dynamic path construction:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Instead of hardcoded paths:
|
||||||
|
// "/ai-voicebot/api/health"
|
||||||
|
|
||||||
|
// The system uses:
|
||||||
|
this.getApiPath("/ai-voicebot/api/health")
|
||||||
|
// Which becomes: `${PUBLIC_URL}/api/health`
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows deployment to different base paths without code changes.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Full Generation (Recommended)
|
||||||
|
```bash
|
||||||
|
./generate-ts-types.sh
|
||||||
|
```
|
||||||
|
This runs the complete pipeline and is the primary way to use the system.
|
||||||
|
|
||||||
|
### Individual Steps
|
||||||
|
```bash
|
||||||
|
# Inside client container
|
||||||
|
npm run generate-schema # Generate OpenAPI schema
|
||||||
|
npm run generate-types # Generate TypeScript types
|
||||||
|
npm run update-api-client # Update API client
|
||||||
|
npm run check-api-evolution # Check for missing endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
## How Auto-Updates Work
|
||||||
|
|
||||||
|
### API Client Updates
|
||||||
|
|
||||||
|
The `update-api-client.js` script:
|
||||||
|
|
||||||
|
1. **Parses OpenAPI schema** to find all available endpoints
|
||||||
|
2. **Scans existing API client** to detect implemented methods
|
||||||
|
3. **Identifies missing endpoints** by comparing the two
|
||||||
|
4. **Generates method implementations** for missing endpoints
|
||||||
|
5. **Updates the client class** by inserting new methods in designated section
|
||||||
|
6. **Updates endpoint lists** used by evolution checking
|
||||||
|
|
||||||
|
#### Auto-Generated Section
|
||||||
|
```typescript
|
||||||
|
export class ApiClient {
|
||||||
|
// ... manual methods ...
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct API path using PUBLIC_URL environment variable
|
||||||
|
* Replaces hardcoded /ai-voicebot prefix with dynamic base from environment
|
||||||
|
*/
|
||||||
|
private getApiPath(schemaPath: string): string {
|
||||||
|
return schemaPath.replace('/ai-voicebot', base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generated endpoints will be added here by update-api-client.js
|
||||||
|
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
||||||
|
|
||||||
|
// New endpoints automatically appear here using this.getApiPath()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Method Generation
|
||||||
|
- **Method names** derived from `operationId` or path/method combination
|
||||||
|
- **Parameters** inferred from path parameters and request body
|
||||||
|
- **Return types** use generic `Promise<any>` (can be enhanced)
|
||||||
|
- **Path handling** supports both static and parameterized paths using `PUBLIC_URL`
|
||||||
|
- **Dynamic paths** automatically replace hardcoded prefixes with environment-based values
|
||||||
|
|
||||||
|
### Evolution Checker Updates
|
||||||
|
|
||||||
|
The evolution checker tracks:
|
||||||
|
- **Known schema endpoints** - updated from current OpenAPI schema
|
||||||
|
- **Implemented endpoints** - updated from actual API client code
|
||||||
|
- **Missing endpoints** - calculated difference for warnings
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Adding Manual Endpoints
|
||||||
|
|
||||||
|
For endpoints not in OpenAPI schema (e.g., external services), add them manually before the auto-generated section:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Manual endpoints (these won't be auto-generated)
|
||||||
|
async getCustomData(): Promise<CustomResponse> {
|
||||||
|
return this.request<CustomResponse>("/custom/endpoint", { method: "GET" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generated endpoints will be added here by update-api-client.js
|
||||||
|
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
||||||
|
```
|
||||||
|
|
||||||
|
### Improving Generated Methods
|
||||||
|
|
||||||
|
To enhance auto-generated methods:
|
||||||
|
|
||||||
|
1. **Better Type Inference**: Modify `generateMethodSignature()` in `update-api-client.js` to use specific types from schema
|
||||||
|
2. **Parameter Validation**: Add validation logic in method generation
|
||||||
|
3. **Error Handling**: Customize error handling patterns
|
||||||
|
4. **Documentation**: Add JSDoc generation from OpenAPI descriptions
|
||||||
|
|
||||||
|
### Schema Evolution Detection
|
||||||
|
|
||||||
|
The system detects:
|
||||||
|
- **New endpoints** added to OpenAPI schema
|
||||||
|
- **Changed endpoints** (parameter or response changes)
|
||||||
|
- **Deprecated endpoints** (with proper OpenAPI marking)
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
1. **Develop API endpoints** in FastAPI server with proper typing
|
||||||
|
2. **Run generation script** to update client: `./generate-ts-types.sh`
|
||||||
|
3. **Use generated types** in React components
|
||||||
|
4. **Manual customization** for complex endpoints if needed
|
||||||
|
5. **Commit all changes** including generated and updated files
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Server Development
|
||||||
|
- Use **Pydantic models** for all request/response types
|
||||||
|
- Add **proper OpenAPI metadata** (summary, description, tags)
|
||||||
|
- Use **consistent naming** for operation IDs
|
||||||
|
- **Version your API** to handle breaking changes
|
||||||
|
|
||||||
|
### Client Development
|
||||||
|
- **Import from api-client.ts** rather than making raw fetch calls
|
||||||
|
- **Use generated types** for type safety
|
||||||
|
- **Avoid editing auto-generated sections** - they will be overwritten
|
||||||
|
- **Add custom endpoints manually** when needed
|
||||||
|
|
||||||
|
### Type Safety
|
||||||
|
```typescript
|
||||||
|
// Good: Using generated types and client
|
||||||
|
import { apiClient, type LobbyModel, type LobbyCreateRequest } from './api-client';
|
||||||
|
|
||||||
|
const createLobby = async (data: LobbyCreateRequest): Promise<LobbyModel> => {
|
||||||
|
const response = await apiClient.createLobby(sessionId, data);
|
||||||
|
return response.data; // Fully typed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Avoid: Direct fetch calls
|
||||||
|
const createLobbyRaw = async () => {
|
||||||
|
const response = await fetch('/api/lobby', { /* ... */ });
|
||||||
|
return response.json(); // No type safety
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**"Could not find insertion marker"**
|
||||||
|
- The API client file was manually edited and the auto-generation markers were removed
|
||||||
|
- Restore the markers or regenerate the client file from template
|
||||||
|
|
||||||
|
**"Missing endpoints detected"**
|
||||||
|
- New endpoints were added to the server but the generation script wasn't run
|
||||||
|
- Run `./generate-ts-types.sh` to update the client
|
||||||
|
|
||||||
|
**"Type errors after generation"**
|
||||||
|
- Schema changes may have affected existing manual code
|
||||||
|
- Check the TypeScript compiler output and update affected code
|
||||||
|
|
||||||
|
**"Duplicate method names"**
|
||||||
|
- Manual methods conflict with auto-generated ones
|
||||||
|
- Rename manual methods or adjust the operation ID generation logic
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
Add debug logging by modifying `update-api-client.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Add after parsing
|
||||||
|
console.log('Schema endpoints:', this.endpoints.map(e => `${e.method}:${e.path}`));
|
||||||
|
console.log('Implemented endpoints:', Array.from(this.implementedEndpoints));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
- **Stronger type inference** from OpenAPI schema components
|
||||||
|
- **Request/response validation** using schema definitions
|
||||||
|
- **Mock data generation** for testing
|
||||||
|
- **API versioning support** with backward compatibility
|
||||||
|
- **Performance optimization** with request caching
|
||||||
|
- **OpenAPI spec validation** before generation
|
||||||
|
|
||||||
|
## Integration with Build Process
|
||||||
|
|
||||||
|
The system integrates with:
|
||||||
|
- **Docker Compose** for cross-container coordination
|
||||||
|
- **npm scripts** for frontend build pipeline
|
||||||
|
- **TypeScript compilation** for type checking
|
||||||
|
- **CI/CD workflows** for automated updates
|
||||||
|
|
||||||
|
This ensures that API changes are automatically reflected in the frontend without manual intervention, reducing development friction and preventing API/client drift.
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* Demo script to show API evolution detection in action
|
* Demo script to show API evolution detection in action
|
||||||
* This simulates having a missing endpoint to demonstrate the output
|
* This simulates having missing endpoints and shows the automated update process
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
@ -37,8 +37,10 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"generate-schema": "cd ../server && python3 generate_schema_simple.py",
|
"generate-schema": "cd ../server && 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",
|
"update-api-client": "node update-api-client.js",
|
||||||
|
"generate-api-types": "npm run generate-schema && npm run generate-types && npm run update-api-client",
|
||||||
"check-api-evolution": "node check-api-evolution.js",
|
"check-api-evolution": "node check-api-evolution.js",
|
||||||
|
"test-dynamic-paths": "node test-dynamic-paths.js",
|
||||||
"prebuild": "npm run generate-api-types"
|
"prebuild": "npm run generate-api-types"
|
||||||
},
|
},
|
||||||
"npmConfig": {
|
"npmConfig": {
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
// TypeScript API client for AI Voicebot server
|
// TypeScript API client for AI Voicebot server
|
||||||
import { components, paths } from "./api-types";
|
import { components, paths } from "./api-types";
|
||||||
|
import { base } from "./Common";
|
||||||
|
|
||||||
// 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"];
|
||||||
export type LobbyListItem = components['schemas']['LobbyListItem'];
|
export type LobbyListItem = components["schemas"]["LobbyListItem"];
|
||||||
export type LobbyCreateData = components['schemas']['LobbyCreateData'];
|
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
|
||||||
export type NamePasswordRecord = components['schemas']['NamePasswordRecord'];
|
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
|
||||||
|
|
||||||
// Type aliases for API methods
|
// Type aliases for API methods
|
||||||
export type AdminNamesResponse = components['schemas']['AdminNamesResponse'];
|
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
|
||||||
export type AdminActionResponse = components['schemas']['AdminActionResponse'];
|
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
|
||||||
export type AdminSetPassword = components['schemas']['AdminSetPassword'];
|
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
|
||||||
export type AdminClearPassword = components['schemas']['AdminClearPassword'];
|
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
|
||||||
export type HealthResponse = components['schemas']['HealthResponse'];
|
export type HealthResponse = components["schemas"]["HealthResponse"];
|
||||||
export type LobbiesResponse = components['schemas']['LobbiesResponse'];
|
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
|
||||||
export type SessionResponse = components['schemas']['SessionResponse'];
|
export type SessionResponse = components["schemas"]["SessionResponse"];
|
||||||
export type LobbyCreateRequest = components['schemas']['LobbyCreateRequest'];
|
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
|
||||||
export type LobbyCreateResponse = components['schemas']['LobbyCreateResponse'];
|
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
||||||
|
|
||||||
// Bot Provider Types (manually defined until API types are regenerated)
|
// Bot Provider Types (manually defined until API types are regenerated)
|
||||||
export interface BotInfoModel {
|
export interface BotInfoModel {
|
||||||
@ -57,13 +58,9 @@ export interface BotJoinLobbyResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(
|
constructor(public status: number, public statusText: string, public data?: any) {
|
||||||
public status: number,
|
|
||||||
public statusText: string,
|
|
||||||
public data?: any
|
|
||||||
) {
|
|
||||||
super(`HTTP ${status}: ${statusText}`);
|
super(`HTTP ${status}: ${statusText}`);
|
||||||
this.name = 'ApiError';
|
this.name = "ApiError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -76,6 +73,15 @@ export class ApiClient {
|
|||||||
this.defaultHeaders = {};
|
this.defaultHeaders = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct API path using PUBLIC_URL environment variable
|
||||||
|
* Replaces hardcoded /ai-voicebot prefix with dynamic base from environment
|
||||||
|
*/
|
||||||
|
private getApiPath(schemaPath: string): string {
|
||||||
|
// Replace the hardcoded /ai-voicebot prefix with the dynamic base
|
||||||
|
return schemaPath.replace("/ai-voicebot", base);
|
||||||
|
}
|
||||||
|
|
||||||
private async request<T>(
|
private async request<T>(
|
||||||
path: string,
|
path: string,
|
||||||
options: {
|
options: {
|
||||||
@ -126,50 +132,70 @@ export class ApiClient {
|
|||||||
|
|
||||||
// Admin API methods
|
// Admin API methods
|
||||||
async adminListNames(): Promise<AdminNamesResponse> {
|
async adminListNames(): Promise<AdminNamesResponse> {
|
||||||
return this.request<AdminNamesResponse>("/ai-voicebot/api/admin/names", { method: "GET" });
|
return this.request<AdminNamesResponse>(this.getApiPath("/ai-voicebot/api/admin/names"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminSetPassword(data: AdminSetPassword): Promise<AdminActionResponse> {
|
async adminSetPassword(data: AdminSetPassword): Promise<AdminActionResponse> {
|
||||||
return this.request<AdminActionResponse>("/ai-voicebot/api/admin/set_password", { method: "POST", body: data });
|
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/set_password"), {
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
|
async adminClearPassword(data: AdminClearPassword): Promise<AdminActionResponse> {
|
||||||
return this.request<AdminActionResponse>("/ai-voicebot/api/admin/clear_password", { method: "POST", body: data });
|
return this.request<AdminActionResponse>(this.getApiPath("/ai-voicebot/api/admin/clear_password"), {
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
async healthCheck(): Promise<HealthResponse> {
|
async healthCheck(): Promise<HealthResponse> {
|
||||||
return this.request<HealthResponse>("/ai-voicebot/api/health", { method: "GET" });
|
return this.request<HealthResponse>(this.getApiPath("/ai-voicebot/api/health"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session methods
|
// Session methods
|
||||||
async getSession(): Promise<SessionResponse> {
|
async getSession(): Promise<SessionResponse> {
|
||||||
return this.request<SessionResponse>("/ai-voicebot/api/session", { method: "GET" });
|
return this.request<SessionResponse>(this.getApiPath("/ai-voicebot/api/session"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lobby methods
|
// Lobby methods
|
||||||
async getLobbies(): Promise<LobbiesResponse> {
|
async getLobbies(): Promise<LobbiesResponse> {
|
||||||
return this.request<LobbiesResponse>("/ai-voicebot/api/lobby", { method: "GET" });
|
return this.request<LobbiesResponse>(this.getApiPath("/ai-voicebot/api/lobby"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async createLobby(sessionId: string, data: LobbyCreateRequest): Promise<LobbyCreateResponse> {
|
async createLobby(sessionId: string, data: LobbyCreateRequest): Promise<LobbyCreateResponse> {
|
||||||
return this.request<LobbyCreateResponse>(`/ai-voicebot/api/lobby/${sessionId}`, { method: "POST", body: data });
|
return this.request<LobbyCreateResponse>(this.getApiPath(`/ai-voicebot/api/lobby/${sessionId}`), {
|
||||||
|
method: "POST",
|
||||||
|
body: data,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bot Provider methods
|
// Bot Provider methods
|
||||||
async getBotProviders(): Promise<BotProviderListResponse> {
|
async getBotProviders(): Promise<BotProviderListResponse> {
|
||||||
return this.request<BotProviderListResponse>("/ai-voicebot/api/bots/providers", { method: "GET" });
|
return this.request<BotProviderListResponse>(this.getApiPath("/ai-voicebot/api/bots/providers"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAvailableBots(): Promise<BotListResponse> {
|
async getAvailableBots(): Promise<BotListResponse> {
|
||||||
return this.request<BotListResponse>("/ai-voicebot/api/bots", { method: "GET" });
|
return this.request<BotListResponse>(this.getApiPath("/ai-voicebot/api/bots"), { method: "GET" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise<BotJoinLobbyResponse> {
|
async requestBotJoinLobby(botName: string, request: BotJoinLobbyRequest): Promise<BotJoinLobbyResponse> {
|
||||||
return this.request<BotJoinLobbyResponse>(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`, {
|
return this.request<BotJoinLobbyResponse>(
|
||||||
method: "POST",
|
this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`),
|
||||||
body: request,
|
{
|
||||||
});
|
method: "POST",
|
||||||
|
body: request,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generated endpoints will be added here by update-api-client.js
|
||||||
|
// DO NOT MANUALLY EDIT BELOW THIS LINE
|
||||||
|
|
||||||
|
// Auto-generated endpoints
|
||||||
|
async lobbyCreate(session_id: string, data: any): Promise<any> {
|
||||||
|
return this.request<any>(this.getApiPath(`/ai-voicebot/api/lobby/${session_id}`), { method: "POST", body: data });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -193,13 +219,17 @@ class ApiEvolutionChecker {
|
|||||||
|
|
||||||
private getImplementedEndpoints(): Set<string> {
|
private getImplementedEndpoints(): Set<string> {
|
||||||
// Define all endpoints that are currently implemented in ApiClient
|
// Define all endpoints that are currently implemented in ApiClient
|
||||||
|
// This list is automatically updated by update-api-client.js
|
||||||
return new Set([
|
return new Set([
|
||||||
'GET:/ai-voicebot/api/admin/names',
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
'POST:/ai-voicebot/api/admin/set_password',
|
'GET:/ai-voicebot/api/bots',
|
||||||
'POST:/ai-voicebot/api/admin/clear_password',
|
'GET:/ai-voicebot/api/bots/providers',
|
||||||
'GET:/ai-voicebot/api/health',
|
'GET:/ai-voicebot/api/health',
|
||||||
'GET:/ai-voicebot/api/session',
|
|
||||||
'GET:/ai-voicebot/api/lobby',
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'GET:/ai-voicebot/api/session',
|
||||||
|
'POST:/ai-voicebot/api/admin/clear_password',
|
||||||
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
||||||
'POST:/ai-voicebot/api/lobby/{session_id}'
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// This module provides runtime introspection of OpenAPI schema changes
|
// This module provides runtime introspection of OpenAPI schema changes
|
||||||
|
|
||||||
import { paths } from './api-types';
|
import { paths } from './api-types';
|
||||||
|
import { base } from "./Common";
|
||||||
|
|
||||||
export interface EndpointInfo {
|
export interface EndpointInfo {
|
||||||
path: string;
|
path: string;
|
||||||
@ -27,10 +28,11 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
*/
|
*/
|
||||||
private extractEndpointsFromSchema(): EndpointInfo[] {
|
private extractEndpointsFromSchema(): EndpointInfo[] {
|
||||||
const endpoints: EndpointInfo[] = [];
|
const endpoints: EndpointInfo[] = [];
|
||||||
|
|
||||||
// In a real implementation, we would need to parse the actual OpenAPI JSON
|
// 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
|
// 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.
|
// a list that should be updated when new endpoints are added to the schema.
|
||||||
|
// This list is automatically updated by the update-api-client.js script
|
||||||
const knownSchemaEndpoints = [
|
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/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/set_password', method: 'POST', operationId: 'admin_set_password_ai_voicebot_api_admin_set_password_post' },
|
||||||
@ -38,17 +40,17 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
{ path: '/ai-voicebot/api/health', method: 'GET', operationId: 'health_ai_voicebot_api_health_get' },
|
{ 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/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', 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' },
|
{ path: '/ai-voicebot/api/lobby/{session_id}', method: 'POST', operationId: 'lobby_create_ai_voicebot_api_lobby__session_id__post' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Get implemented endpoints from ApiClient
|
// Get implemented endpoints from ApiClient
|
||||||
const implementedEndpoints = this.getImplementedEndpoints();
|
const implementedEndpoints = this.getImplementedEndpoints();
|
||||||
|
|
||||||
knownSchemaEndpoints.forEach(endpoint => {
|
knownSchemaEndpoints.forEach((endpoint) => {
|
||||||
const key = `${endpoint.method}:${endpoint.path}`;
|
const key = `${endpoint.method}:${endpoint.path}`;
|
||||||
endpoints.push({
|
endpoints.push({
|
||||||
...endpoint,
|
...endpoint,
|
||||||
implemented: implementedEndpoints.has(key)
|
implemented: implementedEndpoints.has(key),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -58,11 +60,14 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
private getImplementedEndpoints(): Set<string> {
|
private getImplementedEndpoints(): Set<string> {
|
||||||
return new Set([
|
return new Set([
|
||||||
'GET:/ai-voicebot/api/admin/names',
|
'GET:/ai-voicebot/api/admin/names',
|
||||||
'POST:/ai-voicebot/api/admin/set_password',
|
'POST:/ai-voicebot/api/admin/set_password',
|
||||||
'POST:/ai-voicebot/api/admin/clear_password',
|
'POST:/ai-voicebot/api/admin/clear_password',
|
||||||
'GET:/ai-voicebot/api/health',
|
'GET:/ai-voicebot/api/health',
|
||||||
'GET:/ai-voicebot/api/session',
|
'GET:/ai-voicebot/api/session',
|
||||||
'GET:/ai-voicebot/api/lobby',
|
'GET:/ai-voicebot/api/lobby',
|
||||||
|
'POST:/ai-voicebot/api/lobby/{sessionId}',
|
||||||
|
'GET:/ai-voicebot/api/bots/providers',
|
||||||
|
'GET:/ai-voicebot/api/bots',
|
||||||
'POST:/ai-voicebot/api/lobby/{session_id}'
|
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@ -72,8 +77,8 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
*/
|
*/
|
||||||
async loadSchemaFromJson(): Promise<any> {
|
async loadSchemaFromJson(): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// In a real implementation, you might fetch this from a URL or import it
|
// Use dynamic base path from environment
|
||||||
const response = await fetch("/ai-voicebot/openapi-schema.json");
|
const response = await fetch(`${base}/openapi-schema.json`);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
@ -118,26 +123,24 @@ export class AdvancedApiEvolutionChecker {
|
|||||||
// Extract endpoints from the actual schema
|
// Extract endpoints from the actual schema
|
||||||
if (schema.paths) {
|
if (schema.paths) {
|
||||||
Object.keys(schema.paths).forEach(path => {
|
Object.keys(schema.paths).forEach(path => {
|
||||||
if (path === '/ai-voicebot/{path}') return; // Skip generic proxy
|
if (path === `${base}/{path}`) return; // Skip generic proxy
|
||||||
|
|
||||||
const pathObj = schema.paths[path];
|
const pathObj = schema.paths[path];
|
||||||
Object.keys(pathObj).forEach(method => {
|
Object.keys(pathObj).forEach((method) => {
|
||||||
const endpoint = `${method.toUpperCase()}:${path}`;
|
const endpoint = `${method.toUpperCase()}:${path}`;
|
||||||
const implementedEndpoints = this.getImplementedEndpoints();
|
const implementedEndpoints = this.getImplementedEndpoints();
|
||||||
|
|
||||||
if (!implementedEndpoints.has(endpoint)) {
|
if (!implementedEndpoints.has(endpoint)) {
|
||||||
const endpointInfo: EndpointInfo = {
|
const endpointInfo: EndpointInfo = {
|
||||||
path,
|
path,
|
||||||
method: method.toUpperCase(),
|
method: method.toUpperCase(),
|
||||||
operationId: pathObj[method].operationId,
|
operationId: pathObj[method].operationId,
|
||||||
implemented: false
|
implemented: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if this is a new endpoint (not in our known list)
|
// Check if this is a new endpoint (not in our known list)
|
||||||
const isKnown = currentEndpoints.some(ep =>
|
const isKnown = currentEndpoints.some((ep) => ep.path === path && ep.method === method.toUpperCase());
|
||||||
ep.path === path && ep.method === method.toUpperCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isKnown) {
|
if (!isKnown) {
|
||||||
hasNewEndpoints = true;
|
hasNewEndpoints = true;
|
||||||
newEndpoints.push(endpointInfo);
|
newEndpoints.push(endpointInfo);
|
||||||
|
152
client/test-dynamic-paths.js
Normal file
152
client/test-dynamic-paths.js
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Test script to verify dynamic path handling in API client
|
||||||
|
* This script tests that the API client correctly uses PUBLIC_URL for dynamic paths
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
class ApiClientPathTester {
|
||||||
|
constructor() {
|
||||||
|
this.clientPath = path.join(__dirname, 'src', 'api-client.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that all API methods use getApiPath for dynamic paths
|
||||||
|
*/
|
||||||
|
testDynamicPaths() {
|
||||||
|
console.log('🧪 Testing API Client Dynamic Path Usage');
|
||||||
|
console.log('=========================================\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clientContent = fs.readFileSync(this.clientPath, 'utf8');
|
||||||
|
|
||||||
|
// Find all API method calls
|
||||||
|
const apiCallRegex = /this\.request<[^>]*>\(([^,]+),/g;
|
||||||
|
const hardcodedPathRegex = /this\.request<[^>]*>\("\/ai-voicebot/g;
|
||||||
|
const dynamicPathRegex = /this\.request<[^>]*>\(this\.getApiPath\(/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
const allApiCalls = [];
|
||||||
|
const hardcodedCalls = [];
|
||||||
|
const dynamicCalls = [];
|
||||||
|
|
||||||
|
// Find all API calls
|
||||||
|
while ((match = apiCallRegex.exec(clientContent)) !== null) {
|
||||||
|
allApiCalls.push(match[1].trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find hardcoded calls (bad)
|
||||||
|
const hardcodedMatches = [...clientContent.matchAll(hardcodedPathRegex)];
|
||||||
|
|
||||||
|
// Find dynamic calls (good)
|
||||||
|
const dynamicMatches = [...clientContent.matchAll(dynamicPathRegex)];
|
||||||
|
|
||||||
|
console.log('📊 Results:');
|
||||||
|
console.log(` • Total API method calls: ${allApiCalls.length}`);
|
||||||
|
console.log(` • Using this.getApiPath(): ${dynamicMatches.length}`);
|
||||||
|
console.log(` • Using hardcoded paths: ${hardcodedMatches.length}`);
|
||||||
|
|
||||||
|
if (hardcodedMatches.length === 0) {
|
||||||
|
console.log('\n✅ PASS: No hardcoded /ai-voicebot paths found!');
|
||||||
|
console.log(' All API calls use dynamic path construction.');
|
||||||
|
} else {
|
||||||
|
console.log('\n❌ FAIL: Found hardcoded paths that should use getApiPath():');
|
||||||
|
// Show some context for each hardcoded path
|
||||||
|
hardcodedMatches.forEach((match, index) => {
|
||||||
|
const start = Math.max(0, match.index - 50);
|
||||||
|
const end = Math.min(clientContent.length, match.index + 100);
|
||||||
|
const context = clientContent.slice(start, end).replace(/\n/g, '\\n');
|
||||||
|
console.log(` ${index + 1}. ...${context}...`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that imports are correct
|
||||||
|
const hasCommonImport = clientContent.includes('import { base } from "./Common"');
|
||||||
|
const hasGetApiPathMethod = clientContent.includes('getApiPath(schemaPath: string)');
|
||||||
|
|
||||||
|
console.log('\n🔍 Implementation Details:');
|
||||||
|
console.log(` • Imports base from Common.ts: ${hasCommonImport ? '✅' : '❌'}`);
|
||||||
|
console.log(` • Has getApiPath() method: ${hasGetApiPathMethod ? '✅' : '❌'}`);
|
||||||
|
|
||||||
|
if (hasCommonImport && hasGetApiPathMethod && hardcodedMatches.length === 0) {
|
||||||
|
console.log('\n🎉 All tests passed! API client correctly uses dynamic paths.');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ Some issues found. See details above.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that the evolution checker also uses dynamic paths
|
||||||
|
*/
|
||||||
|
testEvolutionChecker() {
|
||||||
|
console.log('\n🧪 Testing Evolution Checker Dynamic Path Usage');
|
||||||
|
console.log('===============================================\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const checkerPath = path.join(__dirname, 'src', 'api-evolution-checker.ts');
|
||||||
|
const checkerContent = fs.readFileSync(checkerPath, 'utf8');
|
||||||
|
|
||||||
|
const hasCommonImport = checkerContent.includes('import { base } from \'./Common\'') ||
|
||||||
|
checkerContent.includes('import { base } from "./Common"');
|
||||||
|
const usesDynamicSchema = checkerContent.includes('`${base}/openapi-schema.json`');
|
||||||
|
const usesDynamicProxy = checkerContent.includes('`${base}/{path}`');
|
||||||
|
|
||||||
|
console.log('📊 Evolution Checker Results:');
|
||||||
|
console.log(` • Imports base from Common.ts: ${hasCommonImport ? '✅' : '❌'}`);
|
||||||
|
console.log(` • Uses dynamic schema path: ${usesDynamicSchema ? '✅' : '❌'}`);
|
||||||
|
console.log(` • Uses dynamic proxy check: ${usesDynamicProxy ? '✅' : '❌'}`);
|
||||||
|
|
||||||
|
if (hasCommonImport && usesDynamicSchema && usesDynamicProxy) {
|
||||||
|
console.log('\n✅ Evolution checker correctly uses dynamic paths!');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ Evolution checker has some hardcoded paths.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Evolution checker test failed:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run all tests
|
||||||
|
*/
|
||||||
|
run() {
|
||||||
|
const apiClientTest = this.testDynamicPaths();
|
||||||
|
const evolutionTest = this.testEvolutionChecker();
|
||||||
|
|
||||||
|
console.log('\n📋 Summary');
|
||||||
|
console.log('===========');
|
||||||
|
console.log(`API Client: ${apiClientTest ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
console.log(`Evolution Checker: ${evolutionTest ? '✅ PASS' : '❌ FAIL'}`);
|
||||||
|
|
||||||
|
if (apiClientTest && evolutionTest) {
|
||||||
|
console.log('\n🎉 All dynamic path tests passed!');
|
||||||
|
console.log('The system correctly uses PUBLIC_URL for dynamic path construction.');
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
console.log('\n⚠️ Some tests failed. Check the details above.');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
if (require.main === module) {
|
||||||
|
const tester = new ApiClientPathTester();
|
||||||
|
const exitCode = tester.run();
|
||||||
|
process.exit(exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiClientPathTester;
|
469
client/update-api-client.js
Normal file
469
client/update-api-client.js
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Automated API Client and Evolution Checker Updater
|
||||||
|
*
|
||||||
|
* This script analyzes the generated OpenAPI schema and automatically updates:
|
||||||
|
* 1. api-client.ts - Adds missing endpoint implementations
|
||||||
|
* 2. api-evolution-checker.ts - Updates known endpoints list
|
||||||
|
*
|
||||||
|
* Run this script after generating TypeScript types from OpenAPI schema.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// File paths
|
||||||
|
const OPENAPI_SCHEMA_PATH = path.join(__dirname, 'openapi-schema.json');
|
||||||
|
const API_TYPES_PATH = path.join(__dirname, 'src', 'api-types.ts');
|
||||||
|
const API_CLIENT_PATH = path.join(__dirname, 'src', 'api-client.ts');
|
||||||
|
const API_EVOLUTION_CHECKER_PATH = path.join(__dirname, 'src', 'api-evolution-checker.ts');
|
||||||
|
|
||||||
|
class ApiClientUpdater {
|
||||||
|
constructor() {
|
||||||
|
this.schema = null;
|
||||||
|
this.endpoints = [];
|
||||||
|
this.implementedEndpoints = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and parse the OpenAPI schema
|
||||||
|
*/
|
||||||
|
loadSchema() {
|
||||||
|
try {
|
||||||
|
const schemaContent = fs.readFileSync(OPENAPI_SCHEMA_PATH, 'utf8');
|
||||||
|
this.schema = JSON.parse(schemaContent);
|
||||||
|
console.log('✅ Loaded OpenAPI schema');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to load OpenAPI schema:', error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract all endpoints from the OpenAPI schema
|
||||||
|
*/
|
||||||
|
extractEndpoints() {
|
||||||
|
if (!this.schema || !this.schema.paths) {
|
||||||
|
console.error('❌ No paths found in schema');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.endpoints = [];
|
||||||
|
|
||||||
|
Object.entries(this.schema.paths).forEach(([path, pathObj]) => {
|
||||||
|
// Skip the generic proxy endpoint
|
||||||
|
if (path === '/ai-voicebot/{path}') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.entries(pathObj).forEach(([method, methodObj]) => {
|
||||||
|
const endpoint = {
|
||||||
|
path,
|
||||||
|
method: method.toUpperCase(),
|
||||||
|
operationId: methodObj.operationId,
|
||||||
|
summary: methodObj.summary,
|
||||||
|
parameters: methodObj.parameters || [],
|
||||||
|
requestBody: methodObj.requestBody,
|
||||||
|
responses: methodObj.responses
|
||||||
|
};
|
||||||
|
|
||||||
|
this.endpoints.push(endpoint);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✅ Extracted ${this.endpoints.length} endpoints from schema`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the current API client to find implemented endpoints
|
||||||
|
*/
|
||||||
|
parseCurrentApiClient() {
|
||||||
|
try {
|
||||||
|
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Find implemented endpoints by looking for method patterns and path literals
|
||||||
|
// Now handles both old hardcoded paths and new this.getApiPath() calls
|
||||||
|
const methodRegex = /async\s+(\w+)\([^)]*\):[^{]*{\s*return\s+this\.request<[^>]*>\(([^,]+),\s*{\s*method:\s*"([^"]+)"/g;
|
||||||
|
|
||||||
|
let match;
|
||||||
|
while ((match = methodRegex.exec(clientContent)) !== null) {
|
||||||
|
const [, methodName, pathExpression, httpMethod] = match;
|
||||||
|
|
||||||
|
// Handle both string literals, template literals, and this.getApiPath() calls
|
||||||
|
let actualPath;
|
||||||
|
|
||||||
|
if (pathExpression.includes('this.getApiPath(')) {
|
||||||
|
// Extract path from this.getApiPath("path") or this.getApiPath(`path`)
|
||||||
|
const getApiPathMatch = pathExpression.match(/this\.getApiPath\(([^)]+)\)/);
|
||||||
|
if (getApiPathMatch) {
|
||||||
|
let innerPath = getApiPathMatch[1];
|
||||||
|
|
||||||
|
// Remove quotes or backticks
|
||||||
|
if ((innerPath.startsWith('"') && innerPath.endsWith('"')) ||
|
||||||
|
(innerPath.startsWith("'") && innerPath.endsWith("'"))) {
|
||||||
|
actualPath = innerPath.slice(1, -1);
|
||||||
|
} else if (innerPath.startsWith('`') && innerPath.endsWith('`')) {
|
||||||
|
// Template literal - convert ${param} back to {param}
|
||||||
|
actualPath = innerPath.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pathExpression.startsWith('"') && pathExpression.endsWith('"')) {
|
||||||
|
// String literal
|
||||||
|
actualPath = pathExpression.slice(1, -1);
|
||||||
|
} else if (pathExpression.startsWith('`') && pathExpression.endsWith('`')) {
|
||||||
|
// Template literal - convert ${param} back to {param}
|
||||||
|
actualPath = pathExpression.slice(1, -1).replace(/\$\{([^}]+)\}/g, '{$1}');
|
||||||
|
} else {
|
||||||
|
// Skip complex expressions we can't easily parse
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualPath) {
|
||||||
|
this.implementedEndpoints.add(`${httpMethod}:${actualPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Found ${this.implementedEndpoints.size} implemented endpoints in API client`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to parse current API client:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate method name from operation ID or path
|
||||||
|
*/
|
||||||
|
generateMethodName(endpoint) {
|
||||||
|
if (endpoint.operationId) {
|
||||||
|
// Convert snake_case operation ID to camelCase
|
||||||
|
return endpoint.operationId
|
||||||
|
.replace(/_ai_voicebot_.*$/, '') // Remove the long suffix
|
||||||
|
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())
|
||||||
|
.replace(/^([a-z])/, (_, letter) => letter.toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: generate from path and method
|
||||||
|
const pathParts = endpoint.path.split('/').filter(part => part && !part.startsWith('{'));
|
||||||
|
const lastPart = pathParts[pathParts.length - 1] || 'resource';
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate parameter types and method signature
|
||||||
|
*/
|
||||||
|
generateMethodSignature(endpoint) {
|
||||||
|
const methodName = this.generateMethodName(endpoint);
|
||||||
|
let params = [];
|
||||||
|
let pathParams = [];
|
||||||
|
|
||||||
|
// Extract path parameters
|
||||||
|
const pathParamMatches = endpoint.path.match(/{([^}]+)}/g);
|
||||||
|
if (pathParamMatches) {
|
||||||
|
pathParamMatches.forEach(param => {
|
||||||
|
const paramName = param.slice(1, -1); // Remove { }
|
||||||
|
pathParams.push(paramName);
|
||||||
|
params.push(`${paramName}: string`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for request body
|
||||||
|
if (endpoint.requestBody) {
|
||||||
|
params.push('data: any'); // Could be more specific with schema analysis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for query parameters
|
||||||
|
if (endpoint.parameters && endpoint.parameters.length > 0) {
|
||||||
|
const queryParams = endpoint.parameters.filter(p => p.in === 'query');
|
||||||
|
if (queryParams.length > 0) {
|
||||||
|
params.push('params?: Record<string, string>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const paramString = params.join(', ');
|
||||||
|
const returnType = 'Promise<any>'; // Could be more specific
|
||||||
|
|
||||||
|
return {
|
||||||
|
methodName,
|
||||||
|
paramString,
|
||||||
|
returnType,
|
||||||
|
pathParams
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate method implementation
|
||||||
|
*/
|
||||||
|
generateMethodImplementation(endpoint) {
|
||||||
|
const { methodName, paramString, returnType, pathParams } = this.generateMethodSignature(endpoint);
|
||||||
|
|
||||||
|
// Use this.getApiPath() to make paths dynamic based on PUBLIC_URL
|
||||||
|
let pathExpression;
|
||||||
|
if (pathParams.length > 0) {
|
||||||
|
// For parameterized paths, we need to use template literals
|
||||||
|
pathExpression = 'this.getApiPath(`' + endpoint.path.replace(/{([^}]+)}/g, '${$1}') + '`)';
|
||||||
|
} else {
|
||||||
|
// For static paths, use string literal
|
||||||
|
pathExpression = `this.getApiPath("${endpoint.path}")`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let requestOptions = `{ method: "${endpoint.method}"`;
|
||||||
|
|
||||||
|
if (endpoint.requestBody) {
|
||||||
|
requestOptions += ', body: data';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoint.parameters && endpoint.parameters.some(p => p.in === 'query')) {
|
||||||
|
requestOptions += ', params';
|
||||||
|
}
|
||||||
|
|
||||||
|
requestOptions += ' }';
|
||||||
|
|
||||||
|
return ` async ${methodName}(${paramString}): ${returnType} {
|
||||||
|
return this.request<any>(${pathExpression}, ${requestOptions});
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the API client with missing endpoints
|
||||||
|
*/
|
||||||
|
updateApiClient() {
|
||||||
|
try {
|
||||||
|
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Find unimplemented endpoints
|
||||||
|
const unimplementedEndpoints = this.endpoints.filter(endpoint => {
|
||||||
|
const key = `${endpoint.method}:${endpoint.path}`;
|
||||||
|
return !this.implementedEndpoints.has(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (unimplementedEndpoints.length === 0) {
|
||||||
|
console.log('✅ All endpoints are already implemented in API client');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`🔧 Adding ${unimplementedEndpoints.length} missing endpoints to API client`);
|
||||||
|
|
||||||
|
// Generate new method implementations
|
||||||
|
const newMethods = unimplementedEndpoints.map(endpoint => {
|
||||||
|
console.log(` • ${endpoint.method} ${endpoint.path} (${this.generateMethodName(endpoint)})`);
|
||||||
|
return this.generateMethodImplementation(endpoint);
|
||||||
|
}).join('\n\n');
|
||||||
|
|
||||||
|
// Find the insertion point (before the auto-generated comment and closing brace)
|
||||||
|
const insertionMarker = ' // Auto-generated endpoints will be added here by update-api-client.js';
|
||||||
|
const insertionIndex = clientContent.indexOf(insertionMarker);
|
||||||
|
|
||||||
|
if (insertionIndex === -1) {
|
||||||
|
throw new Error('Could not find auto-generated endpoints insertion marker in ApiClient class');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the end of the marker line
|
||||||
|
const markerEndIndex = clientContent.indexOf('\n', insertionIndex);
|
||||||
|
const doNotEditLine = clientContent.indexOf('\n', markerEndIndex + 1);
|
||||||
|
|
||||||
|
// Insert the new methods after the "DO NOT MANUALLY EDIT BELOW THIS LINE" comment
|
||||||
|
const updatedContent =
|
||||||
|
clientContent.slice(0, doNotEditLine + 1) +
|
||||||
|
'\n // Auto-generated endpoints\n' +
|
||||||
|
newMethods +
|
||||||
|
'\n' +
|
||||||
|
clientContent.slice(doNotEditLine + 1);
|
||||||
|
|
||||||
|
// Write the updated content
|
||||||
|
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
|
||||||
|
console.log('✅ Updated API client with new endpoints');
|
||||||
|
|
||||||
|
// Also update the implemented endpoints list
|
||||||
|
this.updateApiClientImplementedEndpointsList(unimplementedEndpoints);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to update API client:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the implemented endpoints list in ApiClient
|
||||||
|
*/
|
||||||
|
updateApiClientImplementedEndpointsList(newEndpoints) {
|
||||||
|
try {
|
||||||
|
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Add new endpoints to the implemented list
|
||||||
|
const newEndpointKeys = newEndpoints.map(ep => `${ep.method}:${ep.path}`);
|
||||||
|
const allImplementedEndpoints = Array.from(this.implementedEndpoints)
|
||||||
|
.concat(newEndpointKeys)
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
const implementedArray = allImplementedEndpoints
|
||||||
|
.map(endpoint => ` '${endpoint}'`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const newImplementedEndpoints = `return new Set([
|
||||||
|
${implementedArray}
|
||||||
|
]);`;
|
||||||
|
|
||||||
|
// Replace the existing implemented endpoints set in ApiClient
|
||||||
|
const implementedRegex = /private getImplementedEndpoints\(\): Set<string> \{[\s\S]*?return new Set\(\[[^\]]*\]\);[\s\S]*?\}/;
|
||||||
|
|
||||||
|
const replacement = `private getImplementedEndpoints(): Set<string> {
|
||||||
|
// Define all endpoints that are currently implemented in ApiClient
|
||||||
|
// This list is automatically updated by update-api-client.js
|
||||||
|
${newImplementedEndpoints}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
if (implementedRegex.test(clientContent)) {
|
||||||
|
const updatedContent = clientContent.replace(implementedRegex, replacement);
|
||||||
|
fs.writeFileSync(API_CLIENT_PATH, updatedContent, 'utf8');
|
||||||
|
console.log('✅ Updated implemented endpoints list in API client');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not find getImplementedEndpoints method in API client');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to update implemented endpoints list in API client:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the API evolution checker with current endpoints
|
||||||
|
*/
|
||||||
|
updateEvolutionChecker() {
|
||||||
|
try {
|
||||||
|
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Generate the updated known endpoints list
|
||||||
|
const knownEndpointsArray = this.endpoints.map(endpoint => {
|
||||||
|
return ` { path: '${endpoint.path}', method: '${endpoint.method}', operationId: '${endpoint.operationId || ''}' }`;
|
||||||
|
}).join(',\n');
|
||||||
|
|
||||||
|
const newKnownEndpoints = `const knownSchemaEndpoints = [
|
||||||
|
${knownEndpointsArray}
|
||||||
|
];`;
|
||||||
|
|
||||||
|
// Replace the existing knownSchemaEndpoints array
|
||||||
|
const knownEndpointsRegex = /const knownSchemaEndpoints = \[[^\]]+\];/s;
|
||||||
|
|
||||||
|
if (knownEndpointsRegex.test(checkerContent)) {
|
||||||
|
const updatedContent = checkerContent.replace(knownEndpointsRegex, newKnownEndpoints);
|
||||||
|
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
|
||||||
|
console.log('✅ Updated API evolution checker with current endpoints');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not find knownSchemaEndpoints array in evolution checker');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to update evolution checker:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the implemented endpoints list in the evolution checker
|
||||||
|
*/
|
||||||
|
updateImplementedEndpointsList() {
|
||||||
|
try {
|
||||||
|
const checkerContent = fs.readFileSync(API_EVOLUTION_CHECKER_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Generate the updated implemented endpoints list
|
||||||
|
const implementedArray = Array.from(this.implementedEndpoints)
|
||||||
|
.concat(this.endpoints.map(ep => `${ep.method}:${ep.path}`))
|
||||||
|
.filter((value, index, self) => self.indexOf(value) === index) // Remove duplicates
|
||||||
|
.map(endpoint => ` '${endpoint}'`)
|
||||||
|
.join(',\n');
|
||||||
|
|
||||||
|
const newImplementedEndpoints = `return new Set([
|
||||||
|
${implementedArray}
|
||||||
|
]);`;
|
||||||
|
|
||||||
|
// Replace the existing implemented endpoints set
|
||||||
|
const implementedRegex = /return new Set\(\[[^\]]+\]\);/s;
|
||||||
|
|
||||||
|
if (implementedRegex.test(checkerContent)) {
|
||||||
|
const updatedContent = checkerContent.replace(implementedRegex, newImplementedEndpoints);
|
||||||
|
fs.writeFileSync(API_EVOLUTION_CHECKER_PATH, updatedContent, 'utf8');
|
||||||
|
console.log('✅ Updated implemented endpoints list in evolution checker');
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ Could not find implemented endpoints set in evolution checker');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to update implemented endpoints list:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate updated type exports for the API client
|
||||||
|
*/
|
||||||
|
updateTypeExports() {
|
||||||
|
try {
|
||||||
|
const clientContent = fs.readFileSync(API_CLIENT_PATH, 'utf8');
|
||||||
|
|
||||||
|
// Extract all schema types from the generated api-types.ts
|
||||||
|
const typesContent = fs.readFileSync(API_TYPES_PATH, 'utf8');
|
||||||
|
const schemaRegex = /export interface paths \{[\s\S]*?\n\}/;
|
||||||
|
const componentsRegex = /export interface components \{[\s\S]*schemas: \{[\s\S]*?\n \};\n\}/;
|
||||||
|
|
||||||
|
// Find all schema names in components.schemas
|
||||||
|
const schemaNames = [];
|
||||||
|
const schemaMatches = typesContent.match(/"([^"]+)":\s*\{/g);
|
||||||
|
if (schemaMatches) {
|
||||||
|
schemaMatches.forEach(match => {
|
||||||
|
const name = match.match(/"([^"]+)":/)[1];
|
||||||
|
if (name && !name.includes('ValidationError')) {
|
||||||
|
schemaNames.push(name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ Found ${schemaNames.length} schema types for potential export`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Failed to update type exports:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the complete update process
|
||||||
|
*/
|
||||||
|
async run() {
|
||||||
|
console.log('🚀 Starting automated API client update...\n');
|
||||||
|
|
||||||
|
if (!this.loadSchema()) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extractEndpoints();
|
||||||
|
this.parseCurrentApiClient();
|
||||||
|
|
||||||
|
console.log('\n📊 Analysis Results:');
|
||||||
|
console.log(` • Schema endpoints: ${this.endpoints.length}`);
|
||||||
|
console.log(` • Implemented endpoints: ${this.implementedEndpoints.size}`);
|
||||||
|
console.log(` • Missing endpoints: ${this.endpoints.length - this.implementedEndpoints.size}\n`);
|
||||||
|
|
||||||
|
// Update files
|
||||||
|
this.updateApiClient();
|
||||||
|
this.updateEvolutionChecker();
|
||||||
|
this.updateImplementedEndpointsList();
|
||||||
|
|
||||||
|
console.log('\n✅ API client update complete!');
|
||||||
|
console.log('📄 Updated files:');
|
||||||
|
console.log(' - api-client.ts (added missing endpoints)');
|
||||||
|
console.log(' - api-evolution-checker.ts (updated known endpoints)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the updater
|
||||||
|
if (require.main === module) {
|
||||||
|
const updater = new ApiClientUpdater();
|
||||||
|
updater.run().catch(error => {
|
||||||
|
console.error('❌ Update failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ApiClientUpdater;
|
@ -24,19 +24,26 @@ docker compose exec client npm install --legacy-peer-deps
|
|||||||
echo "📋 Step 4: Generating TypeScript types from OpenAPI schema..."
|
echo "📋 Step 4: Generating TypeScript types from OpenAPI schema..."
|
||||||
docker compose exec client npx openapi-typescript openapi-schema.json -o src/api-types.ts
|
docker compose exec client npx openapi-typescript openapi-schema.json -o src/api-types.ts
|
||||||
|
|
||||||
echo "📋 Step 5: Running TypeScript type checking..."
|
echo "📋 Step 5: Automatically updating API client and evolution checker..."
|
||||||
|
docker compose exec client node update-api-client.js
|
||||||
|
|
||||||
|
echo "📋 Step 6: Running TypeScript type checking..."
|
||||||
docker compose exec client npm run type-check
|
docker compose exec client npm run type-check
|
||||||
|
|
||||||
echo "📋 Step 6: Running API evolution check..."
|
echo "📋 Step 7: Testing dynamic path usage..."
|
||||||
|
docker compose exec client npm run test-dynamic-paths
|
||||||
|
|
||||||
|
echo "📋 Step 8: Running API evolution check..."
|
||||||
docker compose exec client node check-api-evolution.js
|
docker compose exec client node check-api-evolution.js
|
||||||
|
|
||||||
echo "✅ TypeScript generation complete!"
|
echo "✅ TypeScript generation and API client update 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 ""
|
||||||
echo "📄 Manual files (uses generated types):"
|
echo "📄 Updated files:"
|
||||||
echo " - client/src/api-client.ts (API client utilities)"
|
echo " - client/src/api-client.ts (automatically updated with new endpoints)"
|
||||||
|
echo " - client/src/api-evolution-checker.ts (updated known endpoints)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "💡 Usage in your React components:"
|
echo "💡 Usage in your React components:"
|
||||||
echo " import { apiClient, adminApi, healthApi } from './api-client';"
|
echo " import { apiClient, adminApi, healthApi } from './api-client';"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user