470 lines
16 KiB
JavaScript
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;
|