UNPKG

hana-cli

Version:
382 lines (336 loc) 11.8 kB
// @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 = 'erdDiagram' export const aliases = ['erd', 'er', 'schema-diagram', 'entityrelation'] export const describe = baseLite.bundle.getText("erdDiagram") const erdDiagramOptions = { schema: { alias: ['s'], type: 'string', default: '**CURRENT_SCHEMA**', desc: baseLite.bundle.getText("erdDiagramSchema") }, tables: { alias: ['t'], type: 'string', desc: baseLite.bundle.getText("erdDiagramTables") }, output: { alias: ['o'], type: 'string', desc: baseLite.bundle.getText("erdDiagramOutput") }, format: { alias: ['f'], type: 'string', choices: ["mermaid", "plantuml", "graphviz", "json"], default: "mermaid", desc: baseLite.bundle.getText("erdDiagramFormat") }, showCardinality: { alias: ['c'], type: 'boolean', default: true, desc: baseLite.bundle.getText("erdDiagramShowCardinality") }, showColumns: { alias: ['cols'], type: 'boolean', default: true, desc: baseLite.bundle.getText("erdDiagramShowColumns") }, excludeColumns: { alias: ['ec'], type: 'string', desc: baseLite.bundle.getText("erdDiagramExcludeColumns") }, profile: { alias: ['p'], type: 'string', desc: baseLite.bundle.getText("profile") } } export const builder = (yargs) => yargs.options(baseLite.getBuilder(erdDiagramOptions)).wrap(160).example('hana-cli erdDiagram --schema MYSCHEMA --format mermaid --output erd.md', baseLite.bundle.getText('erdDiagramExample')).wrap(160).epilog(buildDocEpilogue('erdDiagram', 'analysis-tools', ['calcViewAnalyzer', 'schemaClone', 'graphWorkspaces'])) export const erdDiagramBuilderOptions = baseLite.getBuilder(erdDiagramOptions) export const inputPrompts = { schema: { description: baseLite.bundle.getText("erdDiagramSchema"), type: 'string', required: false, ask: () => false }, tables: { description: baseLite.bundle.getText("erdDiagramTables"), type: 'string', required: false, ask: () => false }, output: { description: baseLite.bundle.getText("erdDiagramOutput"), type: 'string', required: false, ask: () => false }, format: { description: baseLite.bundle.getText("erdDiagramFormat"), type: 'string', required: false, ask: () => false }, showCardinality: { description: baseLite.bundle.getText("erdDiagramShowCardinality"), type: 'boolean', required: false, ask: () => false }, showColumns: { description: baseLite.bundle.getText("erdDiagramShowColumns"), type: 'boolean', required: false, ask: () => false }, excludeColumns: { description: baseLite.bundle.getText("erdDiagramExcludeColumns"), 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, erdDiagramMain, inputPrompts, true, true, erdDiagramBuilderOptions) } /** * Generate ER diagrams from database schema * @param {object} prompts - User prompts * @returns {Promise<void>} */ export async function erdDiagramMain(prompts) { const base = await import('../utils/base.js') base.debug('erdDiagramMain') 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 || schema === '**CURRENT_SCHEMA**') && dbKind !== 'sqlite') { schema = await getCurrentSchema(dbClient, dbKind) } console.log(baseLite.bundle.getText("info.generatingERDiagram", [schema])) // Get tables let tables = await getTables(dbClient, schema, dbKind) // Filter tables if specified if (prompts.tables) { const tableNames = prompts.tables.split(',').map(t => t.trim()) tables = tables.filter(t => tableNames.includes(t.NAME)) } // Get columns for each table for (const table of tables) { table.columns = await getTableColumns(dbClient, schema, table.NAME, dbKind) } // Get foreign keys const foreignKeys = await getForeignKeys(dbClient, schema, dbKind) // Generate output in requested format let output = '' if (prompts.format === 'mermaid') { output = generateMermaidDiagram(schema, tables, foreignKeys, prompts) } else if (prompts.format === 'plantuml') { output = generatePlantumlDiagram(schema, tables, foreignKeys, prompts) } else if (prompts.format === 'graphviz') { output = generateGraphvizDiagram(schema, tables, foreignKeys, prompts) } else if (prompts.format === 'json') { output = JSON.stringify({ tables, foreignKeys }, null, 2) } // Output results if (prompts.output) { const fs = await import('fs') await fs.promises.writeFile(prompts.output, output, 'utf-8') } else { console.log(output) } await dbClient.disconnect() } catch (error) { console.error(baseLite.bundle.getText("error.erdDiagram", [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 FROM TABLES WHERE SCHEMA_NAME = '${schema}' ORDER BY TABLE_NAME` : `SELECT table_name as NAME FROM information_schema.tables WHERE table_schema = '${schema}' AND table_type = 'BASE TABLE' ORDER BY table_name` return await dbClient.execSQL(query) } /** * Get columns for a 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 getTableColumns(dbClient, schema, table, dbKind) { const query = dbKind === 'hana' ? `SELECT COLUMN_NAME, DATA_TYPE_NAME, IS_NULLABLE FROM TABLE_COLUMNS WHERE SCHEMA_NAME = '${schema}' AND TABLE_NAME = '${table}' ORDER BY POSITION` : `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema = '${schema}' AND table_name = '${table}' ORDER BY ordinal_position` return await dbClient.execSQL(query) } /** * Get foreign keys for schema * @param {object} dbClient - Database client * @param {string} schema - Schema name * @param {string} dbKind - Database kind * @returns {Promise<Array>} */ async function getForeignKeys(dbClient, schema, dbKind) { const query = dbKind === 'hana' ? `SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM REFERENTIAL_CONSTRAINTS c JOIN KEY_COLUMNS kc ON c.CONSTRAINT_NAME = kc.CONSTRAINT_NAME WHERE c.SCHEMA_NAME = '${schema}'` : `SELECT constraint_name, table_name, column_name, referenced_table_name, referenced_column_name FROM information_schema.referential_constraints rc JOIN information_schema.key_column_usage kcu ON rc.constraint_name = kcu.constraint_name WHERE rc.constraint_schema = '${schema}'` try { return await dbClient.execSQL(query) } catch (error) { return [] } } /** * Generate Mermaid ER diagram * @param {string} schema - Schema name * @param {Array} tables - Tables with columns * @param {Array} foreignKeys - Foreign key relationships * @param {object} prompts - User options * @returns {string} */ function generateMermaidDiagram(schema, tables, foreignKeys, prompts) { let diagram = `erDiagram\n` for (const table of tables) { let cols = '' if (prompts.showColumns && table.columns) { cols = ` {\n` for (const col of table.columns) { const nullable = (col.IS_NULLABLE === 'Y' || col.is_nullable === 'YES') ? 'O' : '|' cols += ` ${nullable} ${col.COLUMN_NAME || col.column_name} : "${col.DATA_TYPE_NAME || col.data_type}"\n` } cols += ` }` } diagram += ` ${table.NAME}${cols}\n` } if (prompts.showCardinality && foreignKeys.length > 0) { diagram += `\n` const relationships = new Set() for (const fk of foreignKeys) { const rel = `${fk.TABLE_NAME} ||--o{ ${fk.REFERENCED_TABLE_NAME} : ""` if (!relationships.has(rel)) { diagram += ` ${rel}\n` relationships.add(rel) } } } return diagram } /** * Generate PlantUML diagram * @param {string} schema - Schema name * @param {Array} tables - Tables with columns * @param {Array} foreignKeys - Foreign key relationships * @param {object} prompts - User options * @returns {string} */ function generatePlantumlDiagram(schema, tables, foreignKeys, prompts) { let diagram = `@startuml\n` diagram += `title Database Schema: ${schema}\n\n` for (const table of tables) { diagram += `entity ${table.NAME} {\n` if (prompts.showColumns && table.columns) { for (const col of table.columns) { diagram += ` * ${col.COLUMN_NAME || col.column_name}: ${col.DATA_TYPE_NAME || col.data_type}\n` } } diagram += `}\n\n` } if (prompts.showCardinality && foreignKeys.length > 0) { const relationships = new Set() for (const fk of foreignKeys) { const rel = `${fk.TABLE_NAME} }o--|| ${fk.REFERENCED_TABLE_NAME}` if (!relationships.has(rel)) { diagram += `${rel}\n` relationships.add(rel) } } } diagram += `@enduml` return diagram } /** * Generate Graphviz diagram * @param {string} schema - Schema name * @param {Array} tables - Tables with columns * @param {Array} foreignKeys - Foreign key relationships * @param {object} prompts - User options * @returns {string} */ function generateGraphvizDiagram(schema, tables, foreignKeys, prompts) { let diagram = `digraph G {\n` diagram += ` graph [label="${schema}", labelloc=t];\n` diagram += ` rankdir=LR;\n\n` for (const table of tables) { let label = `<table border="1" cellborder="1" cellspacing="0">\n` label += `<tr><td colspan="2"><b>${table.NAME}</b></td></tr>\n` if (prompts.showColumns && table.columns) { for (const col of table.columns) { const dtype = col.DATA_TYPE_NAME || col.data_type || 'UNKNOWN' label += `<tr><td>${col.COLUMN_NAME || col.column_name}</td><td>${dtype}</td></tr>\n` } } label += `</table>` diagram += ` "${table.NAME}" [shape=plaintext, label=<${label}>];\n` } diagram += `\n` if (prompts.showCardinality && foreignKeys.length > 0) { const relationships = new Set() for (const fk of foreignKeys) { const rel = `"${fk.TABLE_NAME}" -> "${fk.REFERENCED_TABLE_NAME}"` if (!relationships.has(rel)) { diagram += ` ${rel};\n` relationships.add(rel) } } } diagram += `}` return diagram } export default { command, aliases, describe, builder, handler }