hana-cli
Version:
HANA Developer Command Line Interface
317 lines (293 loc) • 9.75 kB
JavaScript
// @ts-check
import * as baseLite from '../utils/base-lite.js'
import dbClientClass from "../utils/database/index.js"
import ExcelJS from 'exceljs'
import { buildDocEpilogue } from '../utils/doc-linker.js'
export const command = 'querySimple'
export const aliases = ['qs', "querysimple"]
export const describe = baseLite.bundle.getText("querySimple")
export const builder = (yargs) => yargs.options(baseLite.getBuilder({
query: {
alias: ['q'],
type: 'string',
desc: baseLite.bundle.getText("query")
},
folder: {
alias: ['f'],
type: 'string',
default: './',
desc: baseLite.bundle.getText("folder")
},
filename: {
alias: ['n'],
type: 'string',
desc: baseLite.bundle.getText("filename")
},
output: {
alias: ['o'],
choices: ["table", "json", "excel", "csv"],
default: "table",
type: 'string',
desc: baseLite.bundle.getText("outputTypeQuery")
},
profile: {
alias: ['p'],
type: 'string',
desc: baseLite.bundle.getText("profile")
}
})).wrap(160).example('hana-cli querySimple --query "SELECT * FROM CUSTOMERS" --output csv', baseLite.bundle.getText('querySimpleExample')).wrap(160).epilog(buildDocEpilogue('querySimple', 'performance-monitoring', ['queryPlan', 'expensiveStatements']))
export let inputPrompts = {
query: {
description: baseLite.bundle.getText("query"),
type: 'string',
required: true
},
folder: {
description: baseLite.bundle.getText("folder"),
type: 'string',
required: true
},
filename: {
description: baseLite.bundle.getText("filename"),
type: 'string',
required: true,
ask: () => {
return false
}
},
output: {
description: baseLite.bundle.getText("outputTypeQuery"),
type: 'string',
required: true
},
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, dbQuery, inputPrompts)
}
/**
* Remove newline characters from data row values
* @param {object} dataRow - Data row object
* @returns {object} - Cleaned data row
*/
export function removeNewlineCharacter(dataRow) {
let newDataRow = {}
Object.keys(dataRow).forEach((key) => {
if (typeof dataRow[key] === "string") {
newDataRow[key] = dataRow[key].replace(/[\n\r]+/g, ' ')
} else {
newDataRow[key] = dataRow[key];
}
})
return newDataRow
}
/**
* Execute a simple database query and output results in various formats
* @param {object} prompts - Input prompts with query, output format, and file options
* @returns {Promise<any>}
*/
export async function dbQuery(prompts) {
const base = await import('../utils/base.js')
base.debug('dbQuery')
const [{ highlight }, { AsyncParser }] = await Promise.all([
import('cli-highlight'),
import('@json2csv/node')
])
const opts = { delimiter: ";", transforms: [removeNewlineCharacter] }
const transformOpts = {}
const asyncOpts = {}
// @ts-ignore
const parser = new AsyncParser(opts, transformOpts, asyncOpts)
try {
const dbClient = await dbClientClass.getNewClient(prompts)
await dbClient.connect()
let results = await dbClient.execSQL(prompts.query)
if (typeof results === 'number') {
const response = { rowsAffected: results }
if (prompts.output === 'json') {
console.log(JSON.stringify(response, null, 2))
} else {
console.log(baseLite.bundle.getText("rowsAffected", [results]))
}
await dbClient.disconnect()
return response
}
if (!Array.isArray(results) || results.length === 0) {
const response = { success: true, message: baseLite.bundle.getText("stmtExecutedNoResultSet") }
if (prompts.output === 'json') {
console.log(JSON.stringify(response, null, 2))
} else {
console.log(response.message)
}
await dbClient.disconnect()
return response
}
switch (prompts.output) {
case 'excel':
if (prompts.filename) {
const [{ default: fs }, { default: path }] = await Promise.all([
import('fs'),
import('path')
])
const workbook = new ExcelJS.Workbook()
const worksheet = workbook.addWorksheet('Query Results')
// Add header row with bold formatting
const headers = Object.keys(results[0])
const headerRow = worksheet.addRow(headers)
headerRow.font = { bold: true }
// Add data rows
for (let item of results) {
const rowData = headers.map(key => item[key])
worksheet.addRow(rowData)
}
// Freeze the header row
worksheet.views = [{ state: 'frozen', ySplit: 1 }]
// Ensure directory exists
const dir = prompts.folder
!fs.existsSync(dir) && fs.mkdirSync(dir)
const filename = `${prompts.filename}.xlsx`
const filePath = path.join(dir, filename)
// Write to file
await workbook.xlsx.writeFile(filePath)
console.log(`${baseLite.bundle.getText("contentWritten")}: ${filePath}`)
} else {
base.error(baseLite.bundle.getText("errExcel"))
await dbClient.disconnect()
return
}
break
case 'json':
if (prompts.filename) {
await toFile(prompts.folder, prompts.filename, 'json', JSON.stringify(results, null, 2))
} else {
console.log(highlight(JSON.stringify(results, null, 2)))
await dbClient.disconnect()
return JSON.stringify(results, null, 2)
}
break
case 'csv':
if (prompts.filename) {
const csv = await parser.parse(results).promise()
await toFile(prompts.folder, prompts.filename, 'csv', csv)
} else {
const csv = await parser.parse(results).promise()
console.log(highlight(csv))
await dbClient.disconnect()
return csv
}
break
default:
if (prompts.filename) {
// Format results as a simple text table for file output
const textTable = formatAsTextTable(results)
await toFile(prompts.folder, prompts.filename, 'txt', textTable)
} else {
base.outputTableFancy(results)
}
break
}
await dbClient.disconnect()
return results
} catch (error) {
await base.error(error)
}
}
async function toFile(folder, file, ext, content) {
const base = await import('../utils/base.js')
base.debug('toFile')
const [{ default: fs }, { default: path }] = await Promise.all([
import('fs'),
import('path')
])
let dir = folder
!fs.existsSync(dir) && fs.mkdirSync(dir)
file = `${file}.${ext}`
let fileLocal = path.join(dir, file)
fs.writeFileSync(fileLocal, content)
console.log(`${baseLite.bundle.getText("contentWritten")}: ${fileLocal}`)
}
/**
* Format a value based on its type for text output
* @param {*} val - Value to format
* @returns {string} Formatted value
*/
function formatValue(val) {
if (val === null || val === undefined) {
return ''
}
// Handle Date objects and ISO date strings
if (val instanceof Date) {
return val.toISOString()
}
if (typeof val === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(val)) {
// ISO date string - format it nicely
return new Date(val).toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, '')
}
// Format numbers with locale-appropriate separators
if (typeof val === 'number') {
// Check if it's an integer or has decimals
return Number.isInteger(val) ? val.toLocaleString() : val.toLocaleString(undefined, { maximumFractionDigits: 4 })
}
// Handle boolean values
if (typeof val === 'boolean') {
return val ? 'true' : 'false'
}
// Handle objects and arrays by converting to JSON
if (typeof val === 'object') {
return JSON.stringify(val)
}
return String(val)
}
/**
* Format JSON results as a simple text table with type-aware formatting
* @param {Array<Object>} results - Array of objects to format
* @returns {string} Formatted table string
*/
function formatAsTextTable(results) {
if (!results || results.length === 0) {
return baseLite.bundle.getText('noData')
}
// Get all unique column names
const columns = [...new Set(results.flatMap(row => Object.keys(row)))]
// Calculate column widths with max width limit
const MAX_COL_WIDTH = 50
const widths = {}
columns.forEach(col => {
const maxContentWidth = Math.max(
col.length,
...results.map(row => formatValue(row[col]).length)
)
// Limit column width to prevent overly wide tables
widths[col] = Math.min(maxContentWidth, MAX_COL_WIDTH)
})
// Build header
const header = columns.map(col => col.padEnd(widths[col])).join(' | ')
const separator = columns.map(col => '-'.repeat(widths[col])).join('-+-')
// Build rows with type-aware formatting
const rows = results.map(row =>
columns.map(col => {
const formatted = formatValue(row[col])
// Truncate if exceeds max width
const truncated = formatted.length > MAX_COL_WIDTH
? formatted.substring(0, MAX_COL_WIDTH - 3) + '...'
: formatted
return truncated.padEnd(widths[col])
}).join(' | ')
)
// Add summary for large datasets
const summary = results.length > 1000
? `\n\n[Total rows: ${results.length.toLocaleString()}]`
: ''
return [header, separator, ...rows].join('\n') + summary
}