UNPKG

apx-toolkit

Version:

Automatically discover APIs and generate complete integration packages: code in 12 languages, TypeScript types, test suites, SDK packages, API documentation, mock servers, performance reports, and contract tests. Saves 2-4 weeks of work in seconds.

258 lines 8.71 kB
import { detectRateLimits } from './rate-limit-detector.js'; /** * Checks if a response matches the criteria for an API endpoint */ export async function isAPIResponse(response, config, cachedBody) { const url = response.url(); const headers = response.headers(); const contentType = headers['content-type'] || ''; // Must be JSON response if (!contentType.includes('application/json')) { return false; } // Check URL patterns if provided if (config.apiPatterns && config.apiPatterns.length > 0) { const matchesPattern = config.apiPatterns.some((pattern) => url.includes(pattern)); if (!matchesPattern) { return false; } } // Check response size (use cached body if available) try { const body = cachedBody || await response.body(); if (config.minResponseSize && body.length < config.minResponseSize) { return false; } } catch (error) { // If we can't read the body, skip this response return false; } // Exclude common non-data endpoints const excludePatterns = [ '/config', '/settings', '/manifest', '/health', '/status', '.json', // Often config files ]; const shouldExclude = excludePatterns.some((pattern) => url.includes(pattern)); if (shouldExclude) { return false; } return true; } /** * Extracts API metadata from a network response */ export async function extractAPIMetadata(response, config, cachedBody) { try { const url = new URL(response.url()); const headers = response.headers(); const method = response.request().method(); // Extract base URL (without query parameters) const baseUrl = `${url.protocol}//${url.host}${url.pathname}`; // Extract query parameters const queryParams = {}; url.searchParams.forEach((value, key) => { queryParams[key] = value; }); // Extract headers (especially authentication) const relevantHeaders = {}; const headerKeys = [ 'authorization', 'x-api-key', 'x-auth-token', 'cookie', 'referer', 'origin', 'user-agent', ]; headerKeys.forEach((key) => { const value = headers[key.toLowerCase()]; if (value) { relevantHeaders[key] = value; } }); // Detect rate limiting information const rateLimitInfo = detectRateLimits(response); // Try to extract pagination info from response body let paginationInfo; let dataPath = config.dataPath; try { // Use cached body if available to avoid re-reading const body = cachedBody || await response.body(); const json = JSON.parse(body.toString()); // Auto-detect data path if not provided if (!dataPath) { dataPath = detectDataPath(json); } // Extract pagination information paginationInfo = extractPaginationInfo(json, queryParams); } catch (error) { // If we can't parse JSON, still return the API metadata // The handler will handle parsing errors } // Extract request body if POST let body; let isGraphQL = false; let graphQLQuery; let graphQLOperationName; if (method === 'POST') { try { const postData = response.request().postData(); if (postData) { const parsedBody = JSON.parse(postData); body = parsedBody; // Detect GraphQL requests if (isGraphQLRequest(parsedBody)) { isGraphQL = true; graphQLQuery = typeof parsedBody.query === 'string' ? parsedBody.query : undefined; graphQLOperationName = typeof parsedBody.operationName === 'string' ? parsedBody.operationName : undefined; } } } catch (error) { // Ignore parsing errors } } return { url: response.url(), baseUrl, method, headers: relevantHeaders, queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined, body, paginationInfo, dataPath, rateLimitInfo, isGraphQL, graphQLQuery, graphQLOperationName, }; } catch (error) { return null; } } /** * Auto-detects the path to data items in the JSON response */ function detectDataPath(json) { // Common patterns if (json.data?.items && Array.isArray(json.data.items)) { return 'data.items'; } if (json.data?.results && Array.isArray(json.data.results)) { return 'data.results'; } if (json.data?.list && Array.isArray(json.data.list)) { return 'data.list'; } if (json.results && Array.isArray(json.results)) { return 'results'; } if (json.items && Array.isArray(json.items)) { return 'items'; } // Look for any array property at root level for (const [key, value] of Object.entries(json)) { if (Array.isArray(value) && value.length > 0) { return key; } } return undefined; } /** * Extracts pagination information from API response */ function extractPaginationInfo(json, queryParams) { const info = {}; // Extract from meta object if (json.meta) { if (json.meta.total !== undefined) { info.totalRecords = json.meta.total; } if (json.meta.page !== undefined) { info.currentPage = json.meta.page; info.type = 'page'; info.paramName = 'page'; } if (json.meta.offset !== undefined) { info.currentOffset = json.meta.offset; info.type = 'offset'; info.paramName = 'offset'; } if (json.meta.limit !== undefined) { info.pageSize = json.meta.limit; } if (json.meta.hasNext !== undefined) { info.hasNext = json.meta.hasNext; } if (json.meta.nextCursor) { info.nextCursor = json.meta.nextCursor; info.type = 'cursor'; info.paramName = 'cursor'; } } // Extract from pagination object if (json.pagination) { if (json.pagination.page !== undefined) { info.currentPage = json.pagination.page; info.type = 'page'; info.paramName = 'page'; } if (json.pagination.total !== undefined) { info.totalRecords = json.pagination.total; } if (json.pagination.limit !== undefined) { info.pageSize = json.pagination.limit; } } // Calculate total pages if we have total records and page size if (info.totalRecords && info.pageSize) { info.totalPages = Math.ceil(info.totalRecords / info.pageSize); } // Infer from query parameters if not found in response if (!info.type) { if (queryParams.page) { info.type = 'page'; info.currentPage = parseInt(queryParams.page, 10); info.paramName = 'page'; } else if (queryParams.offset) { info.type = 'offset'; info.currentOffset = parseInt(queryParams.offset, 10); info.paramName = 'offset'; } else if (queryParams.cursor) { info.type = 'cursor'; info.nextCursor = queryParams.cursor; info.paramName = 'cursor'; } } // If we still don't have a type, default to page-based if (!info.type) { info.type = 'page'; info.paramName = 'page'; } return Object.keys(info).length > 1 ? info : undefined; } /** * Detects if a request body is a GraphQL request */ function isGraphQLRequest(body) { if (!body || typeof body !== 'object') { return false; } const obj = body; // GraphQL requests typically have 'query', 'operationName', or 'variables' const hasQuery = 'query' in obj && typeof obj.query === 'string'; const hasOperationName = 'operationName' in obj; const hasVariables = 'variables' in obj; // If it has query (required) and at least one other GraphQL field, it's likely GraphQL return hasQuery && (hasOperationName || hasVariables); } //# sourceMappingURL=api-detector.js.map