UNPKG

@ordojs/core

Version:

Core compiler and runtime for OrdoJS framework

358 lines 15.1 kB
/** * @fileoverview OrdoJS RPC Generator - Automatic RPC stub generation */ import {} from '../types/index.js'; /** * Default RPC generator options */ const DEFAULT_RPC_OPTIONS = { endpoint: '/api/rpc', timeout: 30000, retries: 3, errorHandling: 'throw', authentication: false, compression: false }; /** * RPC Generator for automatic stub generation */ export class RPCGenerator { options; indentLevel = 0; constructor(options = {}) { this.options = { ...DEFAULT_RPC_OPTIONS, ...options }; } /** * Generate RPC stubs for all public server functions in a component */ generateRPCStubs(ast) { if (!ast.component.serverBlock) { return []; } const publicFunctions = ast.component.serverBlock.functions.filter(f => f.isPublic); const stubs = []; for (const func of publicFunctions) { const metadata = { componentName: ast.component.name, functionName: func.name, parameters: func.parameters, returnType: func.returnType, isAsync: func.isAsync, middleware: func.middleware, permissions: func.permissions }; const stub = { functionName: func.name, clientCode: this.generateClientStub(metadata), serverEndpoint: this.generateServerEndpoint(metadata), metadata }; stubs.push(stub); } return stubs; } /** * Generate client-side RPC stub code */ generateClientStub(metadata) { this.indentLevel = 0; const lines = []; // Generate function signature const paramString = this.generateParameterString(metadata.parameters); const asyncKeyword = metadata.isAsync ? 'async ' : ''; lines.push(`${asyncKeyword}function ${metadata.functionName}(${paramString}) {`); this.indentLevel++; // Add parameter validation if needed if (metadata.parameters.length > 0) { lines.push(this.indent('// Parameter validation')); for (const param of metadata.parameters) { if (!param.isOptional) { lines.push(this.indent(`if (${param.name} === undefined || ${param.name} === null) {`)); this.indentLevel++; lines.push(this.indent(`throw new Error('Required parameter "${param.name}" is missing');`)); this.indentLevel--; lines.push(this.indent('}')); } } lines.push(''); } // Generate request payload lines.push(this.indent('// Prepare request payload')); const payloadParams = metadata.parameters.map(p => p.name).join(', '); lines.push(this.indent(`const payload = { ${payloadParams} };`)); lines.push(''); // Generate fetch configuration lines.push(this.indent('// Configure request')); lines.push(this.indent('const requestConfig = {')); this.indentLevel++; lines.push(this.indent('method: "POST",')); lines.push(this.indent('headers: {')); this.indentLevel++; lines.push(this.indent('"Content-Type": "application/json",')); if (this.options.authentication) { lines.push(this.indent('"Authorization": `Bearer ${getAuthToken()}`,')); } if (this.options.compression) { lines.push(this.indent('"Accept-Encoding": "gzip, deflate, br",')); } this.indentLevel--; lines.push(this.indent('},')); lines.push(this.indent('body: JSON.stringify(payload)')); this.indentLevel--; lines.push(this.indent('};')); lines.push(''); // Add timeout if specified if (this.options.timeout && this.options.timeout > 0) { lines.push(this.indent('// Add timeout')); lines.push(this.indent('const controller = new AbortController();')); lines.push(this.indent(`const timeoutId = setTimeout(() => controller.abort(), ${this.options.timeout});`)); lines.push(this.indent('requestConfig.signal = controller.signal;')); lines.push(''); } // Generate retry logic if (this.options.retries && this.options.retries > 0) { lines.push(this.indent('// Retry logic')); lines.push(this.indent(`let retries = ${this.options.retries};`)); lines.push(this.indent('let lastError;')); lines.push(''); lines.push(this.indent('while (retries >= 0) {')); this.indentLevel++; lines.push(this.indent('try {')); this.indentLevel++; } // Generate the actual fetch call const endpoint = `${this.options.endpoint}/${metadata.componentName}/${metadata.functionName}`; lines.push(this.indent(`const response = await fetch("${endpoint}", requestConfig);`)); if (this.options.timeout && this.options.timeout > 0) { lines.push(this.indent('clearTimeout(timeoutId);')); } // Handle response lines.push(''); lines.push(this.indent('// Handle response')); lines.push(this.indent('if (!response.ok) {')); this.indentLevel++; if (this.options.errorHandling === 'throw') { lines.push(this.indent('const errorText = await response.text();')); lines.push(this.indent('throw new Error(`RPC call failed: ${response.status} ${response.statusText}. ${errorText}`);')); } else if (this.options.errorHandling === 'return-null') { lines.push(this.indent('console.error(`RPC call failed: ${response.status} ${response.statusText}`);')); lines.push(this.indent('return null;')); } else if (this.options.errorHandling === 'return-error') { lines.push(this.indent('const errorText = await response.text();')); lines.push(this.indent('return { error: true, status: response.status, message: errorText };')); } this.indentLevel--; lines.push(this.indent('}')); lines.push(''); // Parse and return response lines.push(this.indent('// Parse response')); lines.push(this.indent('const result = await response.json();')); lines.push(this.indent('return result;')); // Close retry logic if enabled if (this.options.retries && this.options.retries > 0) { this.indentLevel--; lines.push(this.indent('} catch (error) {')); this.indentLevel++; lines.push(this.indent('lastError = error;')); lines.push(this.indent('retries--;')); lines.push(this.indent('if (retries < 0) throw lastError;')); lines.push(this.indent('// Wait before retry')); lines.push(this.indent('await new Promise(resolve => setTimeout(resolve, 1000 * (3 - retries)));')); this.indentLevel--; lines.push(this.indent('}')); this.indentLevel--; lines.push(this.indent('}')); } this.indentLevel--; lines.push('}'); return lines.join('\n'); } /** * Generate server endpoint path */ generateServerEndpoint(metadata) { return `${this.options.endpoint}/${metadata.componentName}/${metadata.functionName}`; } /** * Generate Express.js route handler for server function */ generateServerRouteHandler(metadata) { this.indentLevel = 0; const lines = []; const endpoint = this.generateServerEndpoint(metadata); lines.push(`// RPC endpoint for ${metadata.componentName}.${metadata.functionName}`); lines.push(`app.post('${endpoint}', async (req, res) => {`); this.indentLevel++; // Add middleware checks if (metadata.middleware.length > 0) { lines.push(this.indent('// Apply middleware')); for (const middleware of metadata.middleware) { lines.push(this.indent(`await ${middleware}(req, res);`)); } lines.push(''); } // Add permission checks if (metadata.permissions.length > 0) { lines.push(this.indent('// Check permissions')); lines.push(this.indent('const userPermissions = req.user?.permissions || [];')); lines.push(this.indent(`const requiredPermissions = ${JSON.stringify(metadata.permissions)};`)); lines.push(this.indent('const hasPermission = requiredPermissions.every(perm => userPermissions.includes(perm));')); lines.push(this.indent('if (!hasPermission) {')); this.indentLevel++; lines.push(this.indent('return res.status(403).json({ error: "Insufficient permissions" });')); this.indentLevel--; lines.push(this.indent('}')); lines.push(''); } // Extract parameters from request body lines.push(this.indent('try {')); this.indentLevel++; if (metadata.parameters.length > 0) { lines.push(this.indent('// Extract parameters')); const paramNames = metadata.parameters.map(p => p.name); lines.push(this.indent(`const { ${paramNames.join(', ')} } = req.body;`)); lines.push(''); // Validate required parameters for (const param of metadata.parameters) { if (!param.isOptional) { lines.push(this.indent(`if (${param.name} === undefined || ${param.name} === null) {`)); this.indentLevel++; lines.push(this.indent(`return res.status(400).json({ error: "Missing required parameter: ${param.name}" });`)); this.indentLevel--; lines.push(this.indent('}')); } } lines.push(''); } // Call the actual server function lines.push(this.indent('// Call server function')); const paramString = metadata.parameters.map(p => p.name).join(', '); const awaitKeyword = metadata.isAsync ? 'await ' : ''; lines.push(this.indent(`const result = ${awaitKeyword}${metadata.componentName}Server.${metadata.functionName}(${paramString});`)); lines.push(''); // Return result lines.push(this.indent('// Return result')); lines.push(this.indent('res.json(result);')); // Error handling this.indentLevel--; lines.push(this.indent('} catch (error) {')); this.indentLevel++; lines.push(this.indent('console.error(`RPC call error in ${metadata.componentName}.${metadata.functionName}:`, error);')); lines.push(this.indent('res.status(500).json({ error: "Internal server error", message: error.message });')); this.indentLevel--; lines.push(this.indent('}')); this.indentLevel--; lines.push('});'); return lines.join('\n'); } /** * Generate complete RPC client module */ generateRPCClientModule(stubs, componentName) { this.indentLevel = 0; const lines = []; lines.push(`// Auto-generated RPC client for ${componentName}`); lines.push(`// Generated at: ${new Date().toISOString()}`); lines.push(''); // Add authentication helper if needed if (this.options.authentication) { lines.push('// Authentication helper'); lines.push('function getAuthToken() {'); this.indentLevel++; lines.push(this.indent('// Implement your authentication token retrieval logic here')); lines.push(this.indent('return localStorage.getItem("authToken") || sessionStorage.getItem("authToken");')); this.indentLevel--; lines.push('}'); lines.push(''); } // Generate RPC client class lines.push(`export class ${componentName}RPCClient {`); this.indentLevel++; // Add constructor lines.push(this.indent('constructor(options = {}) {')); this.indentLevel++; lines.push(this.indent('this.options = { ...options };')); this.indentLevel--; lines.push(this.indent('}')); lines.push(''); // Add each RPC stub as a method for (const stub of stubs) { lines.push(this.indent(`// ${stub.metadata.functionName}`)); const stubLines = stub.clientCode.split('\n'); for (const stubLine of stubLines) { lines.push(this.indent(stubLine)); } lines.push(''); } this.indentLevel--; lines.push('}'); lines.push(''); // Export default instance lines.push(`export default new ${componentName}RPCClient();`); return lines.join('\n'); } /** * Generate complete server routes module */ generateServerRoutesModule(stubs, componentName) { this.indentLevel = 0; const lines = []; lines.push(`// Auto-generated RPC server routes for ${componentName}`); lines.push(`// Generated at: ${new Date().toISOString()}`); lines.push(''); lines.push(`const ${componentName}Server = require('./${componentName.toLowerCase()}-server');`); lines.push(''); lines.push('module.exports = function(app) {'); this.indentLevel++; for (const stub of stubs) { const routeHandler = this.generateServerRouteHandler(stub.metadata); const handlerLines = routeHandler.split('\n'); for (const handlerLine of handlerLines) { lines.push(this.indent(handlerLine)); } lines.push(''); } this.indentLevel--; lines.push('};'); return lines.join('\n'); } /** * Generate parameter string for function signature */ generateParameterString(parameters) { return parameters.map(param => { let result = param.name; if (param.isOptional) { result += '?'; } if (param.defaultValue) { result += ` = ${this.generateDefaultValue(param.defaultValue)}`; } return result; }).join(', '); } /** * Generate default value for parameter */ generateDefaultValue(defaultValue) { if (typeof defaultValue === 'string') { return `"${defaultValue}"`; } else if (typeof defaultValue === 'object' && defaultValue.expressionType === 'LITERAL') { if (typeof defaultValue.value === 'string') { return `"${defaultValue.value}"`; } return String(defaultValue.value); } return 'undefined'; } /** * Helper to generate indentation */ indent(text) { return ' '.repeat(this.indentLevel) + text; } } //# sourceMappingURL=rpc-generator.js.map