ai-voicebot/client/update-api-client.js
2025-09-03 13:54:29 -07:00

470 lines
16 KiB
JavaScript

#!/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;