UNPKG

@simplyhomes/sos-sdk

Version:

TypeScript SDK for Simply Homes SoS API v4

228 lines (227 loc) 9.61 kB
import { readdirSync, readFileSync, existsSync } from 'fs'; import { resolve } from 'path'; import { normalizeEndpoint } from './utils'; /** * Auto-detect types and methods from generated OpenAPI code * This reads the generated API files and extracts method names, request/response types */ export async function detectTypes() { const typeMap = new Map(); // Path to generated APIs (relative to project root, not compiled location) const apisDir = resolve(process.cwd(), 'src/generated/src/apis'); console.log(` 📁 Looking for APIs in: ${apisDir}`); if (!existsSync(apisDir)) { throw new Error(`Generated APIs directory not found: ${apisDir}\n` + 'Please run "pnpm gen:openapi-types" first to generate the OpenAPI types.'); } const files = readdirSync(apisDir).filter((f) => f.endsWith('.ts') && f !== 'index.ts'); console.log(` 📄 Found ${files.length} API files`); for (const file of files) { const filePath = resolve(apisDir, file); const content = readFileSync(filePath, 'utf-8'); const apiClassName = file.replace('.ts', ''); console.log(` 🔍 Processing ${file}...`); // Extract methods from this API class const methods = extractMethodsFromFile(content, apiClassName); console.log(` ✓ Found ${methods.length} methods`); for (const method of methods) { typeMap.set(method.endpoint, method); } } console.log(` 📊 Total endpoints mapped: ${typeMap.size}`); return typeMap; } /** * Extract method information from a generated API file */ function extractMethodsFromFile(content, apiClassName) { const methods = []; // Find all method signatures in the file // Pattern 1: async methodName(requestParameters: RequestType, ...): Promise<ResponseType> // Also matches: async methodName(requestParameters: RequestType = {}, ...): Promise<ResponseType> // Pattern 2: async methodName(initOverrides?: ...): Promise<ResponseType> (no requestParameters) const methodRegexWithParams = /async\s+(\w+)\(requestParameters:\s*(\w+)(?:\s*=\s*\{[^}]*\})?,.*?\):\s*Promise<(\w+)>/g; const methodRegexWithoutParams = /async\s+(\w+)\(initOverrides\?:.*?\):\s*Promise<(\w+)>/g; let match; let matchCount = 0; // Try with requestParameters first while ((match = methodRegexWithParams.exec(content)) !== null) { matchCount++; const [, methodName, requestType, responseType] = match; // Skip if this is the "Raw" version (we want the main method) if (methodName.endsWith('Raw')) { continue; } // Extract endpoint from the method implementation const endpoint = extractEndpointFromMethod(content, methodName); if (endpoint) { methods.push({ methodName, requestType, responseType, apiClass: apiClassName, endpoint, }); } else { console.log(` ⚠️ Method ${methodName} has no endpoint detected`); } } // Also try methods without requestParameters (like EntitiesApi) while ((match = methodRegexWithoutParams.exec(content)) !== null) { matchCount++; const [, methodName, responseType] = match; // Skip if this is the "Raw" version if (methodName.endsWith('Raw')) { continue; } // For methods without params, use special marker const requestType = 'NoParams'; // Extract endpoint from the method implementation const endpoint = extractEndpointFromMethod(content, methodName); if (endpoint) { methods.push({ methodName, requestType, // Special marker to indicate no request type responseType, apiClass: apiClassName, endpoint, }); } else { console.log(` ⚠️ Method ${methodName} (no params) has no endpoint detected`); } } if (matchCount === 0) { // Debug: check if file has async methods at all const asyncCount = (content.match(/async\s+\w+\(/g) || []).length; console.log(` ⚠️ No matches found. File has ${asyncCount} async methods`); // Try to find first async method to debug const firstAsyncMatch = content.match(/async\s+(\w+)\([^)]+\):\s*Promise<[^>]+>/); if (firstAsyncMatch) { console.log(` 📝 Example method signature: ${firstAsyncMatch[0].substring(0, 100)}...`); } } return methods; } /** * Extract endpoint (HTTP method + path) from method implementation * Looks for the `method:` and `path:` fields in the fetch call */ function extractEndpointFromMethod(content, methodName) { // Find the method implementation section const rawMethodName = `${methodName}Raw`; const methodStartIdx = content.indexOf(`async ${rawMethodName}(`); if (methodStartIdx === -1) { console.log(` ⚠️ Cannot find Raw method for ${methodName}`); return null; } // Find the end of the method - look for the next async method const nextMethodIdx = content.indexOf('\n async ', methodStartIdx + 1); const methodEndIdx = nextMethodIdx === -1 ? content.length : nextMethodIdx; const methodSection = content.substring(methodStartIdx, methodEndIdx); // Extract HTTP method from: method: 'GET', const methodMatch = methodSection.match(/method:\s*['"](\w+)['"],/); const httpMethod = methodMatch ? methodMatch[1].toLowerCase() : null; if (!httpMethod) { console.log(` ⚠️ Cannot find HTTP method for ${methodName}`); return null; } // Extract path from: let urlPath = `/v4/views/{objectName}/schema`; // Pattern: let urlPath = `...` or const urlPath = '...' const pathMatch = methodSection.match(/(?:let|const)\s+urlPath\s*=\s*(`[^`]+`|['"][^'"]+['"])/); if (!pathMatch) { console.log(` ⚠️ Cannot find urlPath for ${methodName}`); return null; } let path = pathMatch[1]; // Remove quotes or backticks path = path.replace(/^[`'"]|[`'"]$/g, ''); // Path already has {objectName} format from generated code, no need to convert return normalizeEndpoint(`${httpMethod} ${path}`); } /** * Detect all available body DTO types from generated models * Returns a Set of body DTO type names (e.g., "V4TransactionsUpdateTransactionBodyDto") */ export function detectBodyDtos() { const bodyDtos = new Set(); const modelsDir = resolve(process.cwd(), 'src/generated/src/models'); if (!existsSync(modelsDir)) { console.log(` ⚠️ Models directory not found: ${modelsDir}`); return bodyDtos; } const files = readdirSync(modelsDir).filter((f) => f.endsWith('BodyDto.ts')); for (const file of files) { // Extract type name from filename (e.g., "V4TransactionsUpdateTransactionBodyDto.ts" → "V4TransactionsUpdateTransactionBodyDto") const typeName = file.replace('.ts', ''); bodyDtos.add(typeName); } console.log(` 📦 Found ${bodyDtos.size} body DTO types`); return bodyDtos; } /** * Detect if a body DTO has a single wrapper property (nested DTO pattern) * * Example nested pattern: * ```typescript * export interface V4TransactionsUpdateTransactionBodyDto { * transaction: V4TransactionsUpdateTransactionBody; * } * ``` * Returns: "transaction" * * Example direct pattern: * ```typescript * export interface V4LeadsUpdateLeadBodyDto { * status?: string; * name?: string; * // ... many properties * } * ``` * Returns: null * * @param bodyTypeName The body DTO type name (e.g., "V4TransactionsUpdateTransactionBodyDto") * @returns The wrapper property name if nested pattern detected, null otherwise */ export function detectDtoWrapperProperty(bodyTypeName) { const modelsDir = resolve(process.cwd(), 'src/generated/src/models'); const dtsFile = resolve(modelsDir, `${bodyTypeName}.ts`); if (!existsSync(dtsFile)) { return null; } try { const content = readFileSync(dtsFile, 'utf-8'); // Find the interface definition - use simpler regex that handles multiline const interfaceRegex = new RegExp(`export interface ${bodyTypeName}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm'); const interfaceMatch = content.match(interfaceRegex); if (!interfaceMatch) { return null; } const interfaceBody = interfaceMatch[1]; // Split by lines and find property declarations const lines = interfaceBody.split('\n'); const properties = []; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines and comment lines if (!trimmed || trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/**')) { continue; } // Match property declaration: "propertyName: Type;" or "propertyName?: Type;" const propMatch = trimmed.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\??:\s*/); if (propMatch) { properties.push(propMatch[1]); } } // Nested pattern: exactly ONE property if (properties.length === 1) { return properties[0]; } return null; } catch (error) { // If we can't read/parse the file, assume no wrapper return null; } }