docs-v2/helper-scripts/influxdb3-plugins/port_to_docs.js

487 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Transforms plugin READMEs from influxdb3_plugins to docs-v2 format.
* Maintains consistency while applying documentation-specific enhancements.
*/
import { promises as fs } from 'fs';
import path from 'path';
import yaml from 'js-yaml';
/**
* Load the mapping configuration file.
*/
async function loadMappingConfig(configPath = 'docs_mapping.yaml') {
try {
const content = await fs.readFile(configPath, 'utf8');
return yaml.load(content);
} catch (error) {
if (error.code === 'ENOENT') {
console.error(`❌ Error: Configuration file '${configPath}' not found`);
} else {
console.error(`❌ Error parsing YAML configuration: ${error.message}`);
}
process.exit(1);
}
}
/**
* Remove the emoji metadata line from content.
*/
function removeEmojiMetadata(content) {
// Remove the emoji line (it's already in the plugin's JSON metadata)
const pattern = /^⚡.*?🔧.*?$\n*/gm;
return content.replace(pattern, '');
}
/**
* Convert relative links to GitHub URLs.
*/
function convertRelativeLinks(content, pluginName) {
const baseUrl = `https://github.com/influxdata/influxdb3_plugins/blob/master/influxdata/${pluginName}/`;
// Convert TOML file links
content = content.replace(
/\[([^\]]+\.toml)\]\(([^)]+\.toml)\)/g,
(match, linkText, linkPath) => `[${linkText}](${baseUrl}${linkPath})`
);
// Convert Python file links
content = content.replace(
/\[([^\]]+\.py)\]\(([^)]+\.py)\)/g,
(match, linkText, linkPath) => `[${linkText}](${baseUrl}${linkPath})`
);
// Convert main README reference
content = content.replace(
'[influxdb3_plugins/README.md](/README.md)',
'[influxdb3_plugins/README.md](https://github.com/influxdata/influxdb3_plugins/blob/master/README.md)'
);
return content;
}
/**
* Replace product references with Hugo shortcodes.
*/
function addProductShortcodes(content) {
// Replace various forms of InfluxDB 3 references
const replacements = [
[/InfluxDB 3 Core\/Enterprise/g, '{{% product-name %}}'],
[/InfluxDB 3 Core and InfluxDB 3 Enterprise/g, '{{% product-name %}}'],
[/InfluxDB 3 Core, InfluxDB 3 Enterprise/g, '{{% product-name %}}'],
// Be careful not to replace in URLs or code blocks
[/(?<!\/)InfluxDB 3(?![/_])/g, '{{% product-name %}}'],
];
for (const [pattern, replacement] of replacements) {
content = content.replace(pattern, replacement);
}
return content;
}
/**
* Optionally enhance the opening description for better SEO.
*/
function enhanceOpeningParagraph(content) {
// This is optional - the source description is usually sufficient
// Only enhance if needed for specific plugins
return content;
}
/**
* Add standard logging section if not present.
*/
function addLoggingSection(content) {
const loggingSection = `
## Logging
Logs are stored in the \`_internal\` database (or the database where the trigger is created) in the \`system.processing_engine_logs\` table. To view logs:
\`\`\`bash
influxdb3 query --database _internal "SELECT * FROM system.processing_engine_logs WHERE trigger_name = 'your_trigger_name'"
\`\`\`
Log columns:
- **event_time**: Timestamp of the log event
- **trigger_name**: Name of the trigger that generated the log
- **log_level**: Severity level (INFO, WARN, ERROR)
- **log_text**: Message describing the action or error`;
// Check if logging section already exists
if (!content.includes('## Logging')) {
// Insert before Questions/Comments section if it exists
if (content.includes('## Questions/Comments')) {
content = content.replace(
'## Questions/Comments',
loggingSection + '\n\n## Questions/Comments'
);
} else {
// Otherwise add before the end
content = content.trimEnd() + '\n' + loggingSection;
}
}
return content;
}
/**
* Replace Questions/Comments section with docs-v2 support sections.
*/
function replaceSupportSection(content) {
const supportSections = `## Report an issue
For plugin issues, see the Plugins repository [issues page](https://github.com/influxdata/influxdb3_plugins/issues).
## Find support for {{% product-name %}}
The [InfluxDB Discord server](https://discord.gg/9zaNCW2PRT) is the best place to find support for InfluxDB 3 Core and InfluxDB 3 Enterprise.
For other InfluxDB versions, see the [Support and feedback](#bug-reports-and-feedback) options.`;
// Remove existing Questions/Comments section
const pattern = /## Questions\/Comments.*?(?=\n##|\n\n##|$)/gs;
content = content.replace(pattern, '');
// Add new support sections
content = content.trimEnd() + '\n\n' + supportSections;
return content;
}
/**
* Ensure code blocks are properly formatted.
*/
function fixCodeBlockFormatting(content) {
// Add bash syntax highlighting where missing
content = content.replace(/```\n(influxdb3 |#)/g, '```bash\n$1');
// Ensure proper spacing around code blocks
content = content.replace(/```\n\n/g, '```\n');
return content;
}
/**
* Add schema requirements section for plugins that need it.
*/
function addSchemaRequirements(content, pluginName) {
// List of plugins that require schema information
const schemaPlugins = ['basic_transformation', 'downsampler'];
if (!schemaPlugins.includes(pluginName)) {
return content;
}
let schemaSection;
if (pluginName === 'basic_transformation') {
schemaSection = `## Schema requirements
The plugin assumes that the table schema is already defined in the database, as it relies on this schema to retrieve field and tag names required for processing.
> [!WARNING]
> #### Requires existing schema
>
> By design, the plugin returns an error if the schema doesn't exist or doesn't contain the expected columns.
`;
} else if (pluginName === 'downsampler') {
schemaSection = `## Schema management
Each downsampled record includes three additional metadata columns:
- \`record_count\` — the number of original points compressed into this single downsampled row
- \`time_from\` — the minimum timestamp among the original points in the interval
- \`time_to\` — the maximum timestamp among the original points in the interval
`;
} else {
return content;
}
// Insert after Configuration section
if (content.includes('## Installation steps')) {
content = content.replace(
'## Installation steps',
schemaSection + '\n## Installation steps'
);
}
return content;
}
/**
* Apply all transformations to convert README for docs-v2.
*/
function transformContent(content, pluginName, config) {
// Apply transformations in order
content = removeEmojiMetadata(content);
content = convertRelativeLinks(content, pluginName);
content = addProductShortcodes(content);
content = enhanceOpeningParagraph(content);
content = fixCodeBlockFormatting(content);
// Add schema requirements if applicable
if (
config.additional_sections &&
config.additional_sections.includes('schema_requirements')
) {
content = addSchemaRequirements(content, pluginName);
}
// Add logging section
content = addLoggingSection(content);
// Replace support section
content = replaceSupportSection(content);
return content;
}
/**
* Process a single plugin README.
* Returns true if successful, false otherwise.
*/
async function processPlugin(pluginName, mapping, dryRun = false) {
const sourcePath = mapping.source;
const targetPath = mapping.target;
try {
// Check if source exists
await fs.access(sourcePath);
} catch (error) {
console.error(`❌ Source not found: ${sourcePath}`);
return false;
}
try {
// Read source content
const content = await fs.readFile(sourcePath, 'utf8');
// Transform content
const transformed = transformContent(content, pluginName, mapping);
if (dryRun) {
console.log(`✅ Would process ${pluginName}`);
console.log(` Source: ${sourcePath}`);
console.log(` Target: ${targetPath}`);
return true;
}
// Ensure target directory exists
await fs.mkdir(path.dirname(targetPath), { recursive: true });
// Write transformed content
await fs.writeFile(targetPath, transformed, 'utf8');
console.log(`✅ Processed ${pluginName}`);
console.log(` Source: ${sourcePath}`);
console.log(` Target: ${targetPath}`);
return true;
} catch (error) {
console.error(`❌ Error processing ${pluginName}: ${error.message}`);
return false;
}
}
/**
* Check if docs-v2 repository is accessible.
*/
async function validateDocsV2Path() {
try {
await fs.access('../..');
return true;
} catch (error) {
console.warn('⚠️ Warning: docs-v2 repository structure not detected');
console.warn(
' Make sure you are running this from docs-v2/helper-scripts/influxdb3-plugins'
);
return false;
}
}
/**
* Parse command line arguments.
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
config: 'docs_mapping.yaml',
plugin: null,
dryRun: false,
validate: false,
help: false,
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
switch (arg) {
case '--config':
options.config = args[++i];
break;
case '--plugin':
options.plugin = args[++i];
break;
case '--dry-run':
options.dryRun = true;
break;
case '--validate':
options.validate = true;
break;
case '--help':
case '-h':
options.help = true;
break;
default:
console.error(`Unknown argument: ${arg}`);
options.help = true;
break;
}
}
return options;
}
/**
* Show help message.
*/
function showHelp() {
console.log(`
Transform plugin READMEs from influxdb3_plugins to docs-v2 format
Usage: node port_to_docs.js [options]
Options:
--config <file> Path to mapping configuration file (default: docs_mapping.yaml)
--plugin <name> Process only specified plugin
--dry-run Show what would be done without making changes
--validate Validate configuration only
--help, -h Show this help message
Examples:
node port_to_docs.js # Process all plugins
node port_to_docs.js --plugin basic_transformation # Process specific plugin
node port_to_docs.js --dry-run # Preview changes
node port_to_docs.js --validate # Check configuration
`);
}
/**
* Main transformation function.
*/
async function main() {
const options = parseArgs();
if (options.help) {
showHelp();
process.exit(0);
}
// Load configuration
const config = await loadMappingConfig(options.config);
if (!config || !config.plugins) {
console.error('❌ Invalid configuration file');
process.exit(1);
}
// Validate configuration
if (options.validate) {
console.log('Validating configuration...');
let valid = true;
for (const [pluginName, mapping] of Object.entries(config.plugins)) {
if (!mapping.source || !mapping.target) {
console.error(
`❌ Invalid mapping for ${pluginName}: missing source or target`
);
valid = false;
continue;
}
try {
await fs.access(mapping.source);
} catch (error) {
console.warn(
`⚠️ Source not found for ${pluginName}: ${mapping.source}`
);
}
}
if (valid) {
console.log('✅ Configuration is valid');
}
process.exit(valid ? 0 : 1);
}
// Check if we're in the right location
if (!options.dryRun && !(await validateDocsV2Path())) {
console.log(
'\nTo use this script, ensure you are in the correct directory:'
);
console.log(' cd docs-v2/helper-scripts/influxdb3-plugins');
process.exit(1);
}
// Process plugins
let pluginsToProcess = Object.entries(config.plugins);
if (options.plugin) {
if (!config.plugins[options.plugin]) {
console.error(`❌ Plugin '${options.plugin}' not found in configuration`);
process.exit(1);
}
pluginsToProcess = [[options.plugin, config.plugins[options.plugin]]];
}
console.log(
`${options.dryRun ? 'DRY RUN: ' : ''}Processing ${pluginsToProcess.length} plugin(s)...\n`
);
let successCount = 0;
let errorCount = 0;
for (const [pluginName, mapping] of pluginsToProcess) {
if (await processPlugin(pluginName, mapping, options.dryRun)) {
successCount++;
} else {
errorCount++;
}
}
// Print summary
console.log('\n' + '='.repeat(60));
console.log('TRANSFORMATION SUMMARY');
console.log('='.repeat(60));
console.log(`Successfully processed: ${successCount}`);
console.log(`Errors: ${errorCount}`);
if (errorCount === 0) {
console.log('\n✅ All plugins processed successfully!');
if (!options.dryRun) {
console.log('\nNext steps:');
console.log('1. Review the generated documentation in docs-v2');
console.log('2. Test that all links work correctly');
console.log('3. Verify product shortcodes render properly');
console.log('4. Commit changes in both repositories');
}
} else {
console.log(`\n${errorCount} plugin(s) failed to process`);
process.exit(1);
}
}
// Handle unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
process.exit(1);
});
// Run main function
if (import.meta.url.endsWith(process.argv[1])) {
main().catch((error) => {
console.error('❌ Fatal error:', error.message);
process.exit(1);
});
}
export { transformContent, processPlugin, loadMappingConfig };