hana-cli
Version:
HANA Developer Command Line Interface
417 lines (369 loc) • 11.8 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 = 'dependencies'
export const aliases = ['deps', 'depend', 'dependency-graph', 'relationships']
export const describe = baseLite.bundle.getText("dependencies")
const dependenciesOptions = {
schema: {
alias: ['s'],
type: 'string',
desc: baseLite.bundle.getText("dependenciesSchema")
},
object: {
alias: ['o'],
type: 'string',
desc: baseLite.bundle.getText("dependenciesObject")
},
direction: {
alias: ['dir'],
type: 'string',
choices: ["incoming", "outgoing", "both"],
default: "both",
desc: baseLite.bundle.getText("dependenciesDirection")
},
depth: {
alias: ['lvl'],
type: 'number',
default: 3,
desc: baseLite.bundle.getText("dependenciesDepth")
},
output: {
alias: ['out'],
type: 'string',
desc: baseLite.bundle.getText("dependenciesOutput")
},
format: {
alias: ['f'],
type: 'string',
choices: ["tree", "json", "graphviz", "mermaid"],
default: "tree",
desc: baseLite.bundle.getText("dependenciesFormat")
},
includeViews: {
alias: ['iv'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("dependenciesIncludeViews")
},
includeProcedures: {
alias: ['ip'],
type: 'boolean',
default: true,
desc: baseLite.bundle.getText("dependenciesIncludeProcedures")
},
profile: {
alias: ['p'],
type: 'string',
desc: baseLite.bundle.getText("profile")
}
}
export const builder = (yargs) => yargs.options(baseLite.getBuilder(dependenciesOptions)).wrap(160).example(
'hana-cli dependencies --object myTable --direction incoming --depth 3',
baseLite.bundle.getText("dependenciesExample")
).epilog(buildDocEpilogue('dependencies', 'analysis-tools', ['objects', 'views', 'procedures']))
export const dependenciesBuilderOptions = baseLite.getBuilder(dependenciesOptions)
export const inputPrompts = {
schema: {
description: baseLite.bundle.getText("dependenciesSchema"),
type: 'string',
required: false,
ask: () => false
},
object: {
description: baseLite.bundle.getText("dependenciesObject"),
type: 'string',
required: true,
ask: () => true
},
direction: {
description: baseLite.bundle.getText("dependenciesDirection"),
type: 'string',
required: false,
ask: () => false
},
depth: {
description: baseLite.bundle.getText("dependenciesDepth"),
type: 'number',
required: false,
ask: () => false
},
output: {
description: baseLite.bundle.getText("dependenciesOutput"),
type: 'string',
required: false,
ask: () => false
},
format: {
description: baseLite.bundle.getText("dependenciesFormat"),
type: 'string',
required: false,
ask: () => false
},
profile: {
description: baseLite.bundle.getText("profile"),
type: 'string',
required: false,
ask: () => { }
},
debug: {
description: baseLite.bundle.getText("debug"),
type: 'boolean',
required: false,
ask: () => false
}
}
/**
* 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, dependenciesMain, inputPrompts, true, true, dependenciesBuilderOptions)
}
/**
* Show object dependency graphs
* @param {object} prompts - User prompts
* @returns {Promise<void>}
*/
export async function dependenciesMain(prompts) {
const base = await import('../utils/base.js')
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 (default to current schema)
let schema = prompts.schema
if (!schema && dbKind !== 'sqlite') {
schema = await getCurrentSchema(dbClient, dbKind)
}
let targetObject = prompts.object
if (!targetObject) {
throw new Error(baseLite.bundle.getText("errObjectRequired"))
}
// Build dependency graph
const graph = {
root: targetObject,
children: [],
parents: [],
allDependencies: new Set()
}
if (['outgoing', 'both'].includes(prompts.direction)) {
const outgoing = await buildDependencyTree(
dbClient, schema, targetObject, 'outgoing', 0, prompts.depth, dbKind, prompts
)
graph.children = outgoing
}
if (['incoming', 'both'].includes(prompts.direction)) {
const incoming = await buildDependencyTree(
dbClient, schema, targetObject, 'incoming', 0, prompts.depth, dbKind, prompts
)
graph.parents = incoming
}
// Generate output in requested format
let output = ''
if (prompts.format === 'tree') {
output = generateTreeOutput(graph)
} else if (prompts.format === 'json') {
output = JSON.stringify(graph, null, 2)
} else if (prompts.format === 'graphviz') {
output = generateGraphvizDependencies(graph)
} else if (prompts.format === 'mermaid') {
output = generateMermaidDependencies(graph)
}
// 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.dependencies", [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'
}
/**
* Build dependency tree recursively
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} objectName - Object name to analyze
* @param {string} direction - 'incoming' or 'outgoing'
* @param {number} currentDepth - Current recursion depth
* @param {number} maxDepth - Maximum recursion depth
* @param {string} dbKind - Database kind
* @param {object} prompts - User options
* @returns {Promise<Array>}
*/
async function buildDependencyTree(dbClient, schema, objectName, direction, currentDepth, maxDepth, dbKind, prompts) {
if (currentDepth >= maxDepth) {
return []
}
const dependencies = await getDependencies(dbClient, schema, objectName, direction, dbKind, prompts)
const dependencyTree = []
for (const dep of dependencies) {
const node = {
name: dep.DEPENDANT || dep.dependant,
type: dep.OBJECT_TYPE || dep.object_type,
children: await buildDependencyTree(
dbClient, schema, dep.DEPENDANT || dep.dependant, direction, currentDepth + 1, maxDepth, dbKind, prompts
)
}
dependencyTree.push(node)
}
return dependencyTree
}
/**
* Get direct dependencies for an object
* @param {object} dbClient - Database client
* @param {string} schema - Schema name
* @param {string} objectName - Object name
* @param {string} direction - 'incoming' or 'outgoing'
* @param {string} dbKind - Database kind
* @param {object} prompts - User options
* @returns {Promise<Array>}
*/
async function getDependencies(dbClient, schema, objectName, direction, dbKind, prompts) {
let query = ''
if (dbKind === 'hana') {
// Try SYS.OBJECT_DEPENDENCIES first
if (direction === 'outgoing') {
query = `SELECT DISTINCT DEPENDENT_OBJECT_NAME as DEPENDANT, DEPENDENT_OBJECT_TYPE as OBJECT_TYPE FROM SYS.OBJECT_DEPENDENCIES WHERE BASE_SCHEMA_NAME = '${schema}' AND BASE_OBJECT_NAME = '${objectName}'`
} else {
query = `SELECT DISTINCT BASE_OBJECT_NAME as DEPENDANT, BASE_OBJECT_TYPE as OBJECT_TYPE FROM SYS.OBJECT_DEPENDENCIES WHERE DEPENDENT_SCHEMA_NAME = '${schema}' AND DEPENDENT_OBJECT_NAME = '${objectName}'`
}
} else {
// Simplified for other databases
query = `SELECT '${objectName}' as dependant, 'TABLE' as object_type`
}
try {
const result = await dbClient.execSQL(query)
return result || []
} catch (error) {
// If SYS.OBJECT_DEPENDENCIES doesn't exist or query fails, try alternative approach
// Return empty array - object exists but no dependencies found
return []
}
}
/**
* Generate tree-formatted output
* @param {object} graph - Dependency graph
* @returns {string}
*/
function generateTreeOutput(graph) {
let output = `\nDependency Graph for: ${graph.root}\n`
output += `${'='.repeat(50)}\n\n`
if (graph.parents.length > 0) {
output += `Dependencies from:\n`
output += formatTreeNodes(graph.parents, 0)
} else {
output += `No incoming dependencies found.\n`
}
output += `\n${graph.root} (Root)\n`
if (graph.children.length > 0) {
output += `\nDependencies to:\n`
output += formatTreeNodes(graph.children, 0)
} else {
output += `\nNo outgoing dependencies found.\n`
}
return output
}
/**
* Format nodes recursively as tree
* @param {Array} nodes - Nodes to format
* @param {number} level - Current indentation level
* @returns {string}
*/
function formatTreeNodes(nodes, level) {
let output = ''
const indent = ' '.repeat(level)
for (const node of nodes) {
output += `${indent}├─ ${node.name} (${node.type})\n`
if (node.children && node.children.length > 0) {
output += formatTreeNodes(node.children, level + 1)
}
}
return output
}
/**
* Generate Graphviz format
* @param {object} graph - Dependency graph
* @returns {string}
*/
function generateGraphvizDependencies(graph) {
let dot = `digraph dependencies {\n`
dot += ` rankdir=LR;\n`
dot += ` "${graph.root}" [shape=box, style=filled, fillcolor=lightblue];\n\n`
if (graph.parents && graph.parents.length > 0) {
for (const parent of graph.parents) {
dot += ` "${parent.name}" -> "${graph.root}";\n`
dot += formatDependencyEdges(parent, graph.root)
}
}
if (graph.children && graph.children.length > 0) {
for (const child of graph.children) {
dot += ` "${graph.root}" -> "${child.name}";\n`
dot += formatDependencyEdges(graph.root, child.name, child)
}
}
dot += `}`
return dot
}
/**
* Format dependency edges recursively
* @param {string} from - From node
* @param {string} to - To node
* @param {object} node - Node object
* @returns {string}
*/
function formatDependencyEdges(from, to, node = null) {
let edges = ''
if (node && node.children && node.children.length > 0) {
for (const child of node.children) {
edges += ` "${to}" -> "${child.name}";\n`
edges += formatDependencyEdges(to, child.name, child)
}
}
return edges
}
/**
* Generate Mermaid format
* @param {object} graph - Dependency graph
* @returns {string}
*/
function generateMermaidDependencies(graph) {
let mermaid = `graph TD\n`
mermaid += ` ${graph.root}["${graph.root}"]\n`
if (graph.parents && graph.parents.length > 0) {
for (const parent of graph.parents) {
mermaid += ` ${parent.name}["${parent.name}"]\n`
mermaid += ` ${parent.name} --> ${graph.root}\n`
}
}
if (graph.children && graph.children.length > 0) {
for (const child of graph.children) {
mermaid += ` ${child.name}["${child.name}"]\n`
mermaid += ` ${graph.root} --> ${child.name}\n`
}
}
return mermaid
}
export default { command, aliases, describe, builder, handler }