hana-cli
Version:
HANA Developer Command Line Interface
444 lines (393 loc) • 13.5 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import dbClientClass from "../utils/database/index.js"
import { buildDocEpilogue } from '../utils/doc-linker.js'
export const command = 'generateDocs'
export const aliases = ['docs', 'gendocs', 'generateDocumentation']
export const describe = baseLite.bundle.getText("generateDocs")
const generateDocsOptions = {
schema: {
alias: ['s'],
type: 'string',
desc: baseLite.bundle.getText("generateDocsSchema")
},
objects: {
alias: ['o'],
type: 'string',
choices: ["tables", "views", "procedures", "functions", "all"],
default: "all",
desc: baseLite.bundle.getText("generateDocsObjects")
},
output: {
alias: ['f'],
type: 'string',
desc: baseLite.bundle.getText("generateDocsOutput")
},
format: {
alias: ['fmt'],
type: 'string',
choices: ["markdown", "html", "pdf"],
default: "markdown",
desc: baseLite.bundle.getText("generateDocsFormat")
},
includeData: {
alias: ['id'],
type: 'boolean',
default: false,
desc: baseLite.bundle.getText("generateDocsIncludeData")
},
includeGrants: {
alias: ['ig'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("generateDocsIncludeGrants")
},
includeIndexes: {
alias: ['ii'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("generateDocsIncludeIndexes")
},
includeTriggers: {
alias: ['it'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("generateDocsIncludeTriggers")
},
generateTOC: {
alias: ['toc'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("generateDocsTOC")
},
profile: {
alias: ['p'],
type: 'string',
desc: baseLite.bundle.getText("profile")
}
}
export const builder = (yargs) => yargs.options(baseLite.getBuilder(generateDocsOptions)).wrap(160).example('hana-cli generateDocs --schema MYSCHEMA --format markdown --output docs/', baseLite.bundle.getText('generateDocsExample')).wrap(160).epilog(buildDocEpilogue('generateDocs', 'developer-tools', ['helpDocu', 'readMe']))
export const generateDocsBuilderOptions = baseLite.getBuilder(generateDocsOptions)
export const inputPrompts = {
schema: {
description: baseLite.bundle.getText("generateDocsSchema"),
type: 'string',
required: false,
ask: () => false
},
objects: {
description: baseLite.bundle.getText("generateDocsObjects"),
type: 'string',
required: false,
ask: () => false
},
output: {
description: baseLite.bundle.getText("generateDocsOutput"),
type: 'string',
required: false,
ask: () => false
},
format: {
description: baseLite.bundle.getText("generateDocsFormat"),
type: 'string',
required: false,
ask: () => false
},
profile: {
description: baseLite.bundle.getText("profile"),
type: 'string',
required: false,
ask: () => { }
}
}
/**
* Command handler function
* @param {object} argv - Command line arguments from yargs
* @returns {Promise<void>}
*/
export async function handler(argv) {
const base = await import('../utils/base.js')
base.promptHandler(argv, generateDocsMain, inputPrompts, true, true, generateDocsBuilderOptions)
}
/**
* Auto-generate database documentation
* @param {object} prompts - User prompts
* @returns {Promise<void>}
*/
export async function generateDocsMain(prompts) {
const base = await import('../utils/base.js')
base.debug('generateDocsMain')
try {
base.setPrompts(prompts)
// Connect to database
const dbClient = await dbClientClass.getNewClient(prompts)
await dbClient.connect()
const dbKind = (dbClient.getKind() || 'hana').toLowerCase()
// Get schema if not provided
let schema = prompts.schema
if (!schema && dbKind !== 'sqlite') {
schema = await getCurrentSchema(dbClient, dbKind)
}
// Gather database objects based on requested types
const docs = {
schema: schema,
tables: [],
views: [],
procedures: [],
functions: [],
generatedAt: new Date().toISOString()
}
if (['tables', 'all'].includes(prompts.objects)) {
docs.tables = await getTables(dbClient, schema, dbKind)
if (prompts.includeIndexes) {
for (const table of docs.tables) {
table.indexes = await getTableIndexes(dbClient, schema, table.NAME, dbKind)
}
}
if (prompts.includeTriggers) {
for (const table of docs.tables) {
table.triggers = await getTableTriggers(dbClient, schema, table.NAME, dbKind)
}
}
}
if (['views', 'all'].includes(prompts.objects)) {
docs.views = await getViews(dbClient, schema, dbKind)
}
if (['procedures', 'all'].includes(prompts.objects)) {
docs.procedures = await getProcedures(dbClient, schema, dbKind)
}
if (['functions', 'all'].includes(prompts.objects)) {
docs.functions = await getFunctions(dbClient, schema, dbKind)
}
// Generate output in requested format
let output = ''
if (prompts.format === 'markdown') {
output = generateMarkdownDocs(docs, prompts.generateTOC)
} else if (prompts.format === 'html') {
output = generateHTMLDocs(docs)
} else if (prompts.format === 'pdf') {
output = generatePDFDocs(docs)
}
// Output results
if (prompts.output) {
const fs = await import('fs')
await fs.promises.writeFile(prompts.output, output, 'utf-8')
console.log(baseLite.bundle.getText("success.docsWritten", [prompts.output]))
} else {
console.log(output)
}
console.log(baseLite.bundle.getText("success.documentationGenerated", [schema]))
await dbClient.disconnect()
} catch (error) {
console.error(baseLite.bundle.getText("error.generateDocs", [error.message]))
process.exit(1)
}
}
/**
* Get current schema
* @param {object} dbClient - Database client
* @param {string} dbKind - Database kind
* @returns {Promise<string>}
*/
async function getCurrentSchema(dbClient, dbKind) {
if (dbKind === 'hana') {
const result = await dbClient.execSQL('SELECT CURRENT_SCHEMA FROM DUMMY')
return result?.[0]?.CURRENT_SCHEMA || 'PUBLIC'
}
return 'public'
}
/**
* Get tables in schema
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getTables(dbClient, schema, dbKind) {
const query = dbKind === 'hana'
? `SELECT TABLE_NAME as NAME, IS_COLUMN_TABLE, COMMENTS FROM TABLES WHERE SCHEMA_NAME = '${schema}' ORDER BY TABLE_NAME`
: `SELECT table_name as NAME, table_type FROM information_schema.tables WHERE table_schema = '${schema}' ORDER BY table_name`
return await dbClient.execSQL(query)
}
/**
* Get views in schema
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getViews(dbClient, schema, dbKind) {
const query = dbKind === 'hana'
? `SELECT VIEW_NAME as NAME, VIEW_TYPE, COMMENTS FROM VIEWS WHERE SCHEMA_NAME = '${schema}' ORDER BY VIEW_NAME`
: `SELECT table_name as NAME FROM information_schema.views WHERE table_schema = '${schema}' ORDER BY table_name`
return await dbClient.execSQL(query)
}
/**
* Get procedures in schema
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getProcedures(dbClient, schema, dbKind) {
const query = dbKind === 'hana'
? `SELECT PROCEDURE_NAME as NAME FROM PROCEDURES WHERE SCHEMA_NAME = '${schema}' ORDER BY PROCEDURE_NAME`
: `SELECT routine_name as NAME FROM information_schema.routines WHERE routine_schema = '${schema}' AND routine_type = 'PROCEDURE' ORDER BY routine_name`
return await dbClient.execSQL(query)
}
/**
* Get functions in schema
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getFunctions(dbClient, schema, dbKind) {
const query = dbKind === 'hana'
? `SELECT FUNCTION_NAME as NAME FROM FUNCTIONS WHERE SCHEMA_NAME = '${schema}' ORDER BY FUNCTION_NAME`
: `SELECT routine_name as NAME FROM information_schema.routines WHERE routine_schema = '${schema}' AND routine_type = 'FUNCTION' ORDER BY routine_name`
return await dbClient.execSQL(query)
}
/**
* Get indexes for table
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} table - Table name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getTableIndexes(dbClient, schema, table, dbKind) {
const query = dbKind === 'hana'
? `SELECT INDEX_NAME, INDEX_TYPE FROM INDEXES WHERE SCHEMA_NAME = '${schema}' AND TABLE_NAME = '${table}'`
: `SELECT indexname FROM pg_indexes WHERE schemaname = '${schema}' AND tablename = '${table}'`
return await dbClient.execSQL(query)
}
/**
* Get triggers for table
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} table - Table name
* @param {string} dbKind - Database kind
* @returns {Promise<Array>}
*/
async function getTableTriggers(dbClient, schema, table, dbKind) {
const query = dbKind === 'hana'
? `SELECT TRIGGER_NAME FROM TRIGGERS WHERE SCHEMA_NAME = '${schema}' AND TABLE_NAME = '${table}'`
: `SELECT trigger_name FROM information_schema.triggers WHERE trigger_schema = '${schema}' AND event_object_table = '${table}'`
return await dbClient.execSQL(query)
}
/**
* Generate markdown documentation
* @param {object} docs - Database documentation
* @param {boolean} generateTOC - Generate table of contents
* @returns {string}
*/
function generateMarkdownDocs(docs, generateTOC) {
let md = `# Database Documentation for ${docs.schema}\n\n`
md += `**Generated:** ${new Date(docs.generatedAt).toLocaleString()}\n\n`
if (generateTOC && (docs.tables.length > 0 || docs.views.length > 0 || docs.procedures.length > 0 || docs.functions.length > 0)) {
md += `## Table of Contents\n\n`
if (docs.tables.length > 0) md += `- [Tables](#tables)\n`
if (docs.views.length > 0) md += `- [Views](#views)\n`
if (docs.procedures.length > 0) md += `- [Procedures](#procedures)\n`
if (docs.functions.length > 0) md += `- [Functions](#functions)\n\n`
}
if (docs.tables.length > 0) {
md += `## Tables\n\n`
for (const table of docs.tables) {
md += `### ${table.NAME}\n`
md += table.COMMENTS ? `${table.COMMENTS}\n\n` : ''
if (table.indexes && table.indexes.length > 0) {
md += `**Indexes:** ${table.indexes.map(i => i.INDEX_NAME || i.indexname).join(', ')}\n`
}
if (table.triggers && table.triggers.length > 0) {
md += `**Triggers:** ${table.triggers.map(t => t.TRIGGER_NAME || t.trigger_name).join(', ')}\n`
}
md += '\n'
}
}
if (docs.views.length > 0) {
md += `## Views\n\n`
for (const view of docs.views) {
md += `### ${view.NAME}\n`
md += view.COMMENTS ? `${view.COMMENTS}\n\n` : ''
}
}
if (docs.procedures.length > 0) {
md += `## Procedures\n\n`
for (const proc of docs.procedures) {
md += `### ${proc.NAME}\n\n`
}
}
if (docs.functions.length > 0) {
md += `## Functions\n\n`
for (const func of docs.functions) {
md += `### ${func.NAME}\n\n`
}
}
return md
}
/**
* Generate HTML documentation
* @param {object} docs - Database documentation
* @returns {string}
*/
function generateHTMLDocs(docs) {
let html = `<!DOCTYPE html>
<html>
<head>
<title>Database Documentation - ${docs.schema}</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #333; }
h2 { color: #666; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
h3 { color: #999; }
table { border-collapse: collapse; margin: 10px 0; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f0f0f0; }
</style>
</head>
<body>
<h1>Database Documentation: ${docs.schema}</h1>
<p><em>Generated: ${new Date(docs.generatedAt).toLocaleString()}</em></p>\n`
if (docs.tables.length > 0) {
html += `<h2>Tables</h2>\n<ul>\n`
for (const table of docs.tables) {
html += `<li><strong>${table.NAME}</strong></li>\n`
}
html += `</ul>\n`
}
if (docs.views.length > 0) {
html += `<h2>Views</h2>\n<ul>\n`
for (const view of docs.views) {
html += `<li><strong>${view.NAME}</strong></li>\n`
}
html += `</ul>\n`
}
if (docs.procedures.length > 0) {
html += `<h2>Procedures</h2>\n<ul>\n`
for (const proc of docs.procedures) {
html += `<li><strong>${proc.NAME}</strong></li>\n`
}
html += `</ul>\n`
}
if (docs.functions.length > 0) {
html += `<h2>Functions</h2>\n<ul>\n`
for (const func of docs.functions) {
html += `<li><strong>${func.NAME}</strong></li>\n`
}
html += `</ul>\n`
}
html += `</body>
</html>`
return html
}
/**
* Generate PDF documentation (returns JSON placeholder)
* @param {object} docs - Database documentation
* @returns {string}
*/
function generatePDFDocs(docs) {
return JSON.stringify(docs, null, 2)
}
export default { command, aliases, describe, builder, handler }