#!/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'); } } const paramString = params.join(', '); const returnType = 'Promise'; // 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(${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 \{[\s\S]*?return new Set\(\[[^\]]*\]\);[\s\S]*?\}/; const replacement = `private getImplementedEndpoints(): Set { // 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;