docs-v2/scripts/docs-cli/lib/api-audit-reporter.js

514 lines
16 KiB
JavaScript

/**
* API audit reporter - generates markdown reports for API documentation coverage
*
* @module api-audit-reporter
*/
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
/**
* Generate API audit report in markdown format
*/
export async function generateAPIAuditReport(comparison, product, version, outputDir) {
const { missing, documented, coverage } = comparison;
const timestamp = new Date().toISOString().split('T')[0];
const filename = `documentation-audit-api-${product}-${version}.md`;
const outputPath = join(outputDir, filename);
// Ensure output directory exists
await fs.mkdir(outputDir, { recursive: true });
const report = [];
// Header
report.push(`# API Documentation Audit - InfluxDB 3 ${product === 'core' ? 'Core' : 'Enterprise'}`);
report.push('');
report.push(`**Version:** ${version}`);
report.push(`**Generated:** ${timestamp}`);
report.push('');
// Coverage Summary
report.push('## Coverage Summary');
report.push('');
report.push(`- **Total Endpoints:** ${coverage.total}`);
report.push(`- **Documented:** ${coverage.documented} (${coverage.percentage}%)`);
report.push(`- **Missing Documentation:** ${coverage.missing}`);
report.push('');
// Coverage by Version
const byVersion = groupByVersion(missing, documented);
report.push('### Coverage by API Version');
report.push('');
report.push('| Version | Total | Documented | Missing | Coverage |');
report.push('|---------|-------|------------|---------|----------|');
for (const [version, stats] of Object.entries(byVersion)) {
const pct = stats.total > 0 ? Math.round((stats.documented / stats.total) * 100) : 0;
report.push(`| ${version} | ${stats.total} | ${stats.documented} | ${stats.missing} | ${pct}% |`);
}
report.push('');
// Coverage by Category
const byCategory = groupByCategory(missing, documented);
report.push('### Coverage by Category');
report.push('');
report.push('| Category | Total | Documented | Missing | Coverage |');
report.push('|----------|-------|------------|---------|----------|');
for (const [category, stats] of Object.entries(byCategory)) {
const pct = stats.total > 0 ? Math.round((stats.documented / stats.total) * 100) : 0;
report.push(`| ${category} | ${stats.total} | ${stats.documented} | ${stats.missing} | ${pct}% |`);
}
report.push('');
// Missing Documentation Section
if (missing.length > 0) {
report.push('## Missing Documentation');
report.push('');
report.push(`The following ${missing.length} endpoints are not documented:`);
report.push('');
// Group missing endpoints by category for better organization
const missingByCategory = groupMissingByCategory(missing);
for (const [category, endpoints] of Object.entries(missingByCategory)) {
if (endpoints.length === 0) continue;
report.push(`### ${capitalizeCategory(category)}`);
report.push('');
for (const endpoint of endpoints) {
report.push(`#### ${endpoint.methods.join(', ')} \`${endpoint.path}\``);
report.push('');
report.push(`- **Description:** ${endpoint.description}`);
report.push(`- **Handler:** \`${endpoint.handler}\``);
report.push(`- **API Version:** ${endpoint.version}`);
report.push('');
}
}
}
// Documented Endpoints Section
if (documented.length > 0) {
report.push('## Documented Endpoints');
report.push('');
report.push(`The following ${documented.length} endpoints have documentation:`);
report.push('');
// Group by version
const documentedByVersion = groupDocumentedByVersion(documented);
for (const [version, endpoints] of Object.entries(documentedByVersion)) {
if (endpoints.length === 0) continue;
report.push(`### API ${version.toUpperCase()}`);
report.push('');
report.push('| Endpoint | Methods | Documentation |');
report.push('|----------|---------|---------------|');
for (const endpoint of endpoints) {
const methods = endpoint.methods.join(', ');
const docFile = endpoint.documentation?.file || 'Unknown';
report.push(`| \`${endpoint.path}\` | ${methods} | ${docFile} |`);
}
report.push('');
}
}
// Endpoints Needing Clarification Section
const needsClarification = [
...missing.filter(e => e.needsClarification),
...documented.filter(e => e.needsClarification)
];
if (needsClarification.length > 0) {
report.push('## Endpoints Needing Clarification');
report.push('');
report.push('The following endpoints may need engineering clarification about their public API status:');
report.push('');
report.push('| Endpoint | Methods | Reason |');
report.push('|----------|---------|--------|');
for (const endpoint of needsClarification) {
const methods = endpoint.methods.join(', ');
report.push(`| \`${endpoint.path}\` | ${methods} | ${endpoint.needsClarification} |`);
}
report.push('');
}
// Recommendations Section
report.push('## Recommendations');
report.push('');
if (missing.length > 0) {
report.push('### Priority Documentation Needs');
report.push('');
// Prioritize v3 API endpoints
const v3Missing = missing.filter(e => e.version === 'v3');
if (v3Missing.length > 0) {
report.push(`1. **V3 API Endpoints** (${v3Missing.length} endpoints)`);
report.push(' - Focus on Core v3 API as it\'s the primary API for InfluxDB 3');
report.push('');
}
// Prioritize write and query endpoints
const criticalMissing = missing.filter(e =>
e.path.includes('write') || e.path.includes('query')
);
if (criticalMissing.length > 0) {
report.push(`2. **Write and Query Endpoints** (${criticalMissing.length} endpoints)`);
report.push(' - These are core functionality endpoints used by all users');
report.push('');
}
// Database and configuration endpoints
const configMissing = missing.filter(e =>
e.path.includes('configure') || e.path.includes('database')
);
if (configMissing.length > 0) {
report.push(`3. **Configuration Endpoints** (${configMissing.length} endpoints)`);
report.push(' - Document database and system configuration endpoints');
report.push('');
}
}
// Write report to file
const reportContent = report.join('\n');
await fs.writeFile(outputPath, reportContent, 'utf-8');
console.log([
`📄 API audit report saved: ${outputPath}`,
`📊 Coverage: ${coverage.percentage}% (${coverage.documented}/${coverage.total} endpoints)`
].join('\n'));
return outputPath;
}
/**
* Group endpoints by API version
*/
function groupByVersion(missing, documented) {
const versions = {};
const allEndpoints = [
...missing.map(e => ({ ...e, isDocumented: false })),
...documented.map(e => ({ ...e, isDocumented: true }))
];
for (const endpoint of allEndpoints) {
const version = endpoint.version;
if (!versions[version]) {
versions[version] = { total: 0, documented: 0, missing: 0 };
}
versions[version].total++;
if (endpoint.isDocumented) {
versions[version].documented++;
} else {
versions[version].missing++;
}
}
return versions;
}
/**
* Group endpoints by category
*/
function groupByCategory(missing, documented) {
const categories = {};
const allEndpoints = [
...missing.map(e => ({ ...e, isDocumented: false })),
...documented.map(e => ({ ...e, isDocumented: true }))
];
for (const endpoint of allEndpoints) {
const category = categorizeEndpoint(endpoint.path);
if (!categories[category]) {
categories[category] = { total: 0, documented: 0, missing: 0 };
}
categories[category].total++;
if (endpoint.isDocumented) {
categories[category].documented++;
} else {
categories[category].missing++;
}
}
return categories;
}
/**
* Group missing endpoints by category
*/
function groupMissingByCategory(missing) {
const byCategory = {
write: [],
query: [],
database: [],
table: [],
cache: [],
'processing-engine': [],
token: [],
health: [],
other: []
};
for (const endpoint of missing) {
const category = categorizeEndpoint(endpoint.path);
byCategory[category].push(endpoint);
}
return byCategory;
}
/**
* Group documented endpoints by version
*/
function groupDocumentedByVersion(documented) {
const byVersion = {
v3: [],
v2: [],
v1: [],
'v1-compat': [],
unversioned: []
};
for (const endpoint of documented) {
byVersion[endpoint.version].push(endpoint);
}
return byVersion;
}
/**
* Categorize endpoint based on path
*/
function categorizeEndpoint(path) {
const lower = path.toLowerCase();
if (lower.includes('write')) return 'write';
if (lower.includes('query')) return 'query';
if (lower.includes('database')) return 'database';
if (lower.includes('table')) return 'table';
if (lower.includes('cache')) return 'cache';
if (lower.includes('processing_engine') || lower.includes('plugin') || lower.includes('engine')) {
return 'processing-engine';
}
if (lower.includes('token')) return 'token';
if (lower.includes('health') || lower.includes('ping') || lower.includes('metrics')) {
return 'health';
}
return 'other';
}
/**
* Capitalize category name for display
*/
function capitalizeCategory(category) {
const names = {
'write': 'Write Endpoints',
'query': 'Query Endpoints',
'database': 'Database Management',
'table': 'Table Management',
'cache': 'Cache Configuration',
'processing-engine': 'Processing Engine',
'token': 'Token Management',
'health': 'Health & Metrics',
'other': 'Other Endpoints'
};
return names[category] || category;
}
/**
* Generate parameter audit report in markdown format
*
* This report highlights API parameters that exist in the source code
* but are not documented in the OpenAPI specs.
*/
export async function generateParameterAuditReport(paramComparison, product, version, outputDir) {
const { endpoints, summary } = paramComparison;
const timestamp = new Date().toISOString().split('T')[0];
const filename = `documentation-audit-api-params-${product}-${version}.md`;
const outputPath = join(outputDir, filename);
// Ensure output directory exists
await fs.mkdir(outputDir, { recursive: true });
const report = [];
// Header
report.push(`# API Parameter Documentation Audit - InfluxDB 3 ${product === 'core' ? 'Core' : 'Enterprise'}`);
report.push('');
report.push(`**Version:** ${version}`);
report.push(`**Generated:** ${timestamp}`);
report.push('');
// Summary
report.push('## Summary');
report.push('');
report.push(`- **Endpoints Analyzed:** ${summary.totalEndpoints}`);
report.push(`- **Endpoints with Missing Parameters:** ${summary.endpointsWithMissingParams}`);
report.push(`- **Total Parameters in Source:** ${summary.totalDiscoveredParams}`);
report.push(`- **Total Parameters in Docs:** ${summary.totalDocumentedParams}`);
report.push(`- **Missing Parameters:** ${summary.totalMissingParams}`);
report.push('');
// Missing Parameters Section
const endpointsWithMissing = endpoints.filter(e => e.missingParams.length > 0);
if (endpointsWithMissing.length > 0) {
report.push('## Missing Parameters');
report.push('');
report.push('The following parameters exist in the source code but are not documented:');
report.push('');
// Group by category
const byCategory = groupEndpointsByCategory(endpointsWithMissing);
for (const [category, categoryEndpoints] of Object.entries(byCategory)) {
if (categoryEndpoints.length === 0) continue;
report.push(`### ${capitalizeCategory(category)}`);
report.push('');
for (const endpoint of categoryEndpoints) {
report.push(`#### ${endpoint.method} \`${endpoint.endpoint}\``);
report.push('');
report.push('| Parameter | Type | Required | Description |');
report.push('|-----------|------|----------|-------------|');
for (const param of endpoint.missingParams) {
const required = param.required ? '✅' : '❌';
const type = formatType(param.type);
const desc = param.description || '_No description_';
report.push(`| \`${param.name}\` | ${type} | ${required} | ${desc} |`);
}
report.push('');
}
}
}
// Detailed Endpoint Analysis
report.push('## Detailed Endpoint Analysis');
report.push('');
for (const endpoint of endpoints) {
if (endpoint.discoveredParams.length === 0) continue;
report.push(`### ${endpoint.method} \`${endpoint.endpoint}\``);
report.push('');
// Show discovered parameters
report.push('**Parameters from Source Code:**');
report.push('');
report.push('| Parameter | Type | Required | Status |');
report.push('|-----------|------|----------|--------|');
for (const param of endpoint.discoveredParams) {
const required = param.required ? '✅' : '❌';
const type = formatType(param.type);
const isMissing = endpoint.missingParams.some(m => m.name === param.name);
const status = isMissing ? '❗ **UNDOCUMENTED**' : '✓ Documented';
report.push(`| \`${param.name}\` | ${type} | ${required} | ${status} |`);
}
report.push('');
}
// Recommendations
report.push('## Recommendations');
report.push('');
if (summary.totalMissingParams > 0) {
report.push('### Priority Updates');
report.push('');
report.push('1. **Update OpenAPI Specs**: Add the missing parameters to the appropriate spec files');
report.push('2. **Document New Features**: Some parameters may be from recently added features');
report.push('3. **Verify Required Fields**: Ensure required/optional status matches implementation');
report.push('');
// Highlight high-priority missing params
const highPriority = endpointsWithMissing.filter(e =>
e.endpoint.includes('processing_engine') ||
e.endpoint.includes('trigger') ||
e.endpoint.includes('write') ||
e.endpoint.includes('query')
);
if (highPriority.length > 0) {
report.push('### High Priority Endpoints');
report.push('');
report.push('These endpoints are frequently used and should be prioritized:');
report.push('');
for (const endpoint of highPriority) {
const params = endpoint.missingParams.map(p => `\`${p.name}\``).join(', ');
report.push(`- **${endpoint.method} ${endpoint.endpoint}**: ${params}`);
}
report.push('');
}
}
// Write report to file
const reportContent = report.join('\n');
await fs.writeFile(outputPath, reportContent, 'utf-8');
console.log([
`📄 Parameter audit report saved: ${outputPath}`,
`📊 Missing parameters: ${summary.totalMissingParams} across ${summary.endpointsWithMissingParams} endpoints`
].join('\n'));
return outputPath;
}
/**
* Group endpoints by category based on path
*/
function groupEndpointsByCategory(endpoints) {
const categories = {
'write': [],
'query': [],
'database': [],
'table': [],
'cache': [],
'processing-engine': [],
'token': [],
'health': [],
'other': []
};
for (const endpoint of endpoints) {
const category = categorizeEndpoint(endpoint.endpoint);
categories[category].push(endpoint);
}
return categories;
}
/**
* Format Rust type for display
*/
function formatType(type) {
if (!type) return 'unknown';
// Simplify common Rust types for display
return type
.replace(/Option<(.+)>/, '$1?')
.replace(/Vec<(.+)>/, '[$1]')
.replace(/HashMap<(.+),\s*(.+)>/, 'Map<$1, $2>')
.replace(/Arc<str>/, 'string')
.replace(/String/, 'string')
.replace(/bool/, 'boolean')
.replace(/u64|i64|u32|i32/, 'integer')
.replace(/f64|f32/, 'number');
}