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
|
||||
/**
|
||||
* 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');
|
||||
|
@ -37,8 +37,10 @@
|
||||
"type-check": "tsc --noEmit",
|
||||
"generate-schema": "cd ../server && python3 generate_schema_simple.py",
|
||||
"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",
|
||||
"test-dynamic-paths": "node test-dynamic-paths.js",
|
||||
"prebuild": "npm run generate-api-types"
|
||||
},
|
||||
"npmConfig": {
|
||||
|
@ -1,22 +1,23 @@
|
||||
// TypeScript API client for AI Voicebot server
|
||||
import { components, paths } from "./api-types";
|
||||
import { base } from "./Common";
|
||||
|
||||
// Re-export commonly used types from the generated schema
|
||||
export type LobbyModel = components['schemas']['LobbyModel'];
|
||||
export type LobbyListItem = components['schemas']['LobbyListItem'];
|
||||
export type LobbyCreateData = components['schemas']['LobbyCreateData'];
|
||||
export type NamePasswordRecord = components['schemas']['NamePasswordRecord'];
|
||||
export type LobbyModel = components["schemas"]["LobbyModel"];
|
||||
export type LobbyListItem = components["schemas"]["LobbyListItem"];
|
||||
export type LobbyCreateData = components["schemas"]["LobbyCreateData"];
|
||||
export type NamePasswordRecord = components["schemas"]["NamePasswordRecord"];
|
||||
|
||||
// Type aliases for API methods
|
||||
export type AdminNamesResponse = components['schemas']['AdminNamesResponse'];
|
||||
export type AdminActionResponse = components['schemas']['AdminActionResponse'];
|
||||
export type AdminSetPassword = components['schemas']['AdminSetPassword'];
|
||||
export type AdminClearPassword = components['schemas']['AdminClearPassword'];
|
||||
export type HealthResponse = components['schemas']['HealthResponse'];
|
||||
export type LobbiesResponse = components['schemas']['LobbiesResponse'];
|
||||
export type SessionResponse = components['schemas']['SessionResponse'];
|
||||
export type LobbyCreateRequest = components['schemas']['LobbyCreateRequest'];
|
||||
export type LobbyCreateResponse = components['schemas']['LobbyCreateResponse'];
|
||||
export type AdminNamesResponse = components["schemas"]["AdminNamesResponse"];
|
||||
export type AdminActionResponse = components["schemas"]["AdminActionResponse"];
|
||||
export type AdminSetPassword = components["schemas"]["AdminSetPassword"];
|
||||
export type AdminClearPassword = components["schemas"]["AdminClearPassword"];
|
||||
export type HealthResponse = components["schemas"]["HealthResponse"];
|
||||
export type LobbiesResponse = components["schemas"]["LobbiesResponse"];
|
||||
export type SessionResponse = components["schemas"]["SessionResponse"];
|
||||
export type LobbyCreateRequest = components["schemas"]["LobbyCreateRequest"];
|
||||
export type LobbyCreateResponse = components["schemas"]["LobbyCreateResponse"];
|
||||
|
||||
// Bot Provider Types (manually defined until API types are regenerated)
|
||||
export interface BotInfoModel {
|
||||
@ -57,13 +58,9 @@ export interface BotJoinLobbyResponse {
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public status: number,
|
||||
public statusText: string,
|
||||
public data?: any
|
||||
) {
|
||||
constructor(public status: number, public statusText: string, public data?: any) {
|
||||
super(`HTTP ${status}: ${statusText}`);
|
||||
this.name = 'ApiError';
|
||||
this.name = "ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,6 +73,15 @@ export class ApiClient {
|
||||
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>(
|
||||
path: string,
|
||||
options: {
|
||||
@ -126,50 +132,70 @@ export class ApiClient {
|
||||
|
||||
// Admin API methods
|
||||
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> {
|
||||
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> {
|
||||
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
|
||||
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
|
||||
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
|
||||
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> {
|
||||
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
|
||||
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> {
|
||||
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> {
|
||||
return this.request<BotJoinLobbyResponse>(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`, {
|
||||
method: "POST",
|
||||
body: request,
|
||||
});
|
||||
return this.request<BotJoinLobbyResponse>(
|
||||
this.getApiPath(`/ai-voicebot/api/bots/${encodeURIComponent(botName)}/join`),
|
||||
{
|
||||
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> {
|
||||
// Define all endpoints that are currently implemented in ApiClient
|
||||
// This list is automatically updated by update-api-client.js
|
||||
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/bots',
|
||||
'GET:/ai-voicebot/api/bots/providers',
|
||||
'GET:/ai-voicebot/api/health',
|
||||
'GET:/ai-voicebot/api/session',
|
||||
'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}'
|
||||
]);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// This module provides runtime introspection of OpenAPI schema changes
|
||||
|
||||
import { paths } from './api-types';
|
||||
import { base } from "./Common";
|
||||
|
||||
export interface EndpointInfo {
|
||||
path: string;
|
||||
@ -27,10 +28,11 @@ export class AdvancedApiEvolutionChecker {
|
||||
*/
|
||||
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.
|
||||
// This list is automatically updated by the update-api-client.js script
|
||||
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' },
|
||||
@ -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/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' },
|
||||
{ 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 => {
|
||||
knownSchemaEndpoints.forEach((endpoint) => {
|
||||
const key = `${endpoint.method}:${endpoint.path}`;
|
||||
endpoints.push({
|
||||
...endpoint,
|
||||
implemented: implementedEndpoints.has(key)
|
||||
implemented: implementedEndpoints.has(key),
|
||||
});
|
||||
});
|
||||
|
||||
@ -58,11 +60,14 @@ export class AdvancedApiEvolutionChecker {
|
||||
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/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/{sessionId}',
|
||||
'GET:/ai-voicebot/api/bots/providers',
|
||||
'GET:/ai-voicebot/api/bots',
|
||||
'POST:/ai-voicebot/api/lobby/{session_id}'
|
||||
]);
|
||||
}
|
||||
@ -72,8 +77,8 @@ export class AdvancedApiEvolutionChecker {
|
||||
*/
|
||||
async loadSchemaFromJson(): Promise<any> {
|
||||
try {
|
||||
// In a real implementation, you might fetch this from a URL or import it
|
||||
const response = await fetch("/ai-voicebot/openapi-schema.json");
|
||||
// Use dynamic base path from environment
|
||||
const response = await fetch(`${base}/openapi-schema.json`);
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
@ -118,26 +123,24 @@ export class AdvancedApiEvolutionChecker {
|
||||
// Extract endpoints from the actual schema
|
||||
if (schema.paths) {
|
||||
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];
|
||||
Object.keys(pathObj).forEach(method => {
|
||||
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
|
||||
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()
|
||||
);
|
||||
|
||||
const isKnown = currentEndpoints.some((ep) => ep.path === path && ep.method === method.toUpperCase());
|
||||
|
||||
if (!isKnown) {
|
||||
hasNewEndpoints = true;
|
||||
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..."
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
echo "✅ TypeScript generation complete!"
|
||||
echo "✅ TypeScript generation and API client update complete!"
|
||||
echo "📄 Generated files:"
|
||||
echo " - client/openapi-schema.json (OpenAPI schema)"
|
||||
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 "📄 Updated files:"
|
||||
echo " - client/src/api-client.ts (automatically updated with new endpoints)"
|
||||
echo " - client/src/api-evolution-checker.ts (updated known endpoints)"
|
||||
echo ""
|
||||
echo "💡 Usage in your React components:"
|
||||
echo " import { apiClient, adminApi, healthApi } from './api-client';"
|
||||
|
Loading…
x
Reference in New Issue
Block a user