Skip to main content
Glama
IBM
by IBM
tableFormatter.ts23.2 kB
/** * @fileoverview Table formatter utility for creating tables in multiple formats. * Supports markdown, ASCII, grid (Unicode), and compact table styles with configurable * alignment, truncation, and formatting options. * @module src/utils/formatting/tableFormatter */ import { JsonRpcErrorCode, McpError } from "@/types-global/errors.js"; import { type RequestContext, logger, requestContextService, } from "@/utils/index.js"; /** * Table output style options. */ export type TableStyle = "markdown" | "ascii" | "grid" | "compact"; /** * Column alignment options. */ export type Alignment = "left" | "center" | "right"; /** * Configuration options for table formatting. */ export interface TableFormatterOptions { /** * Table style to use for rendering. * - markdown: GitHub-style markdown tables (| Header | Header |) * - ascii: ASCII box drawing with +, -, | characters * - grid: Unicode box-drawing characters (┌─┬─┐) * - compact: Space-separated columns (no borders) */ style?: TableStyle; /** * Column-specific alignment configuration. * Key is column name (for object arrays) or index (for raw arrays). * @example { name: 'left', age: 'right', email: 'center' } */ alignment?: Record<string, Alignment>; /** * Maximum width for any column. Content exceeding this will be truncated. */ maxWidth?: number; /** * Whether to truncate long content with ellipsis (default: true). * If false, content may wrap or overflow depending on style. */ truncate?: boolean; /** * Header styling option. * - bold: Headers in bold (markdown only) * - uppercase: Convert headers to uppercase * - none: No special header formatting */ headerStyle?: "bold" | "uppercase" | "none"; /** * Minimum column width (default: 3). */ minWidth?: number; /** * Padding around cell content (default: 1 space). */ padding?: number; /** * String to use for NULL/undefined values (default: '-'). * Use this to display NULL values in a more readable format. */ nullReplacement?: string; } /** * Internal column metadata for rendering. * @private */ interface ColumnInfo { name: string; width: number; alignment: Alignment; } /** * Result from table formatting with metadata. * Includes the formatted table string and statistics about NULL values. */ export interface TableFormattingResult { /** * The formatted table as a string. */ table: string; /** * Metadata about the formatting operation. */ metadata: { /** * Count of NULL/undefined values per column. * Key is column name (or index for raw arrays). */ nullCounts: Record<string, number>; }; } /** * Utility class for formatting tabular data into various table styles. * Provides methods for rendering arrays of objects or raw 2D arrays as tables. */ export class TableFormatter { /** * Default formatting options. * @private */ private readonly defaultOptions: Required<TableFormatterOptions> = { style: "markdown", alignment: {}, maxWidth: 50, truncate: true, headerStyle: "none", minWidth: 3, padding: 1, nullReplacement: "-", }; /** * Track NULL value counts per column during formatting. * Reset before each formatting operation. * @private */ private nullCounts: Record<string, number> = {}; /** * Format an array of objects as a table. * Automatically extracts headers from object keys. * * @template T - Type of objects in the array * @param data - Array of objects to format * @param options - Table formatting options * @param context - Optional request context for logging * @returns Formatted table string * @throws {McpError} If data is invalid or formatting fails * * @example * ```typescript * const data = [ * { name: 'Alice', age: 30, role: 'Engineer' }, * { name: 'Bob', age: 25, role: 'Designer' } * ]; * const table = tableFormatter.format(data, { style: 'grid' }); * ``` */ format<T extends Record<string, unknown>>( data: T[], options?: TableFormatterOptions, context?: RequestContext, ): string { const logContext = context || requestContextService.createRequestContext({ operation: "TableFormatter.format", }); if (!Array.isArray(data)) { throw new McpError( JsonRpcErrorCode.ValidationError, "Data must be an array", logContext, ); } if (data.length === 0) { logger.debug(logContext, "Empty data array provided to table formatter"); return ""; } // Extract headers from first object const headers = Object.keys(data[0]!); // Merge options with defaults const opts: Required<TableFormatterOptions> = { ...this.defaultOptions, ...options, alignment: { ...this.defaultOptions.alignment, ...options?.alignment }, }; // Convert objects to 2D array (without NULL tracking for backward compatibility) const rows = data.map((obj) => headers.map((header) => this.stringify(obj[header], opts.nullReplacement), ), ); logger.debug( { ...logContext, rowCount: rows.length, columnCount: headers.length, }, "Formatting table from object array", ); return this.formatRaw(headers, rows, options, context); } /** * Format a raw 2D array with explicit headers. * Provides full control over headers and cell values. * * @param headers - Array of column headers * @param rows - 2D array of cell values * @param options - Table formatting options * @param context - Optional request context for logging * @returns Formatted table string * @throws {McpError} If headers/rows are invalid or formatting fails * * @example * ```typescript * const headers = ['Name', 'Age', 'Role']; * const rows = [ * ['Alice', '30', 'Engineer'], * ['Bob', '25', 'Designer'] * ]; * const table = tableFormatter.formatRaw(headers, rows, { style: 'ascii' }); * ``` */ formatRaw( headers: string[], rows: string[][], options?: TableFormatterOptions, context?: RequestContext, ): string { const logContext = context || requestContextService.createRequestContext({ operation: "TableFormatter.formatRaw", }); // Validate inputs if (!Array.isArray(headers) || headers.length === 0) { throw new McpError( JsonRpcErrorCode.ValidationError, "Headers must be a non-empty array", logContext, ); } if (!Array.isArray(rows)) { throw new McpError( JsonRpcErrorCode.ValidationError, "Rows must be an array", logContext, ); } if (rows.length === 0) { logger.debug(logContext, "Empty rows array provided to table formatter"); return ""; } // Validate row lengths const columnCount = headers.length; for (let i = 0; i < rows.length; i++) { if (rows[i]!.length !== columnCount) { throw new McpError( JsonRpcErrorCode.ValidationError, `Row ${i} has ${rows[i]!.length} columns but expected ${columnCount}`, { ...logContext, rowIndex: i, expectedColumns: columnCount }, ); } } // Merge options with defaults const opts: Required<TableFormatterOptions> = { ...this.defaultOptions, ...options, alignment: { ...this.defaultOptions.alignment, ...options?.alignment }, }; // Apply header styling const styledHeaders = this.applyHeaderStyle(headers, opts.headerStyle); // Calculate column widths and metadata const columns = this.calculateColumns( styledHeaders, rows, opts, logContext, ); // Render table based on style try { const result = this.renderTable(columns, styledHeaders, rows, opts); logger.debug( { ...logContext, style: opts.style, rows: rows.length, columns: columns.length, }, "Table formatted successfully", ); return result; } catch (error: unknown) { const err = error as Error; logger.error( { ...logContext, error: err.message, }, "Failed to render table", ); throw new McpError( JsonRpcErrorCode.InternalError, `Failed to render table: ${err.message}`, { ...logContext, originalError: err.stack }, ); } } /** * Format an array of objects as a table with NULL value tracking. * Returns both the formatted table and metadata about NULL values. * * @template T - Type of objects in the array * @param data - Array of objects to format * @param options - Table formatting options * @param context - Optional request context for logging * @returns Object containing formatted table and metadata * @throws {McpError} If data is invalid or formatting fails * * @example * ```typescript * const data = [ * { name: 'Alice', age: 30, salary: null }, * { name: 'Bob', age: 25, salary: 50000 } * ]; * const result = tableFormatter.formatWithMetadata(data, { style: 'grid' }); * console.log(result.table); * console.log(result.metadata.nullCounts); // { salary: 1 } * ``` */ formatWithMetadata<T extends Record<string, unknown>>( data: T[], options?: TableFormatterOptions, context?: RequestContext, ): TableFormattingResult { const logContext = context || requestContextService.createRequestContext({ operation: "TableFormatter.formatWithMetadata", }); if (!Array.isArray(data)) { throw new McpError( JsonRpcErrorCode.ValidationError, "Data must be an array", logContext, ); } if (data.length === 0) { logger.debug(logContext, "Empty data array provided to table formatter"); return { table: "", metadata: { nullCounts: {} }, }; } // Reset NULL tracking this.nullCounts = {}; // Extract headers from first object const headers = Object.keys(data[0]!); // Merge options with defaults const opts: Required<TableFormatterOptions> = { ...this.defaultOptions, ...options, alignment: { ...this.defaultOptions.alignment, ...options?.alignment }, }; // Convert objects to 2D array WITH NULL tracking const rows = data.map((obj) => headers.map((header) => this.stringify(obj[header], opts.nullReplacement, header), ), ); logger.debug( { ...logContext, rowCount: rows.length, columnCount: headers.length, nullCounts: this.nullCounts, }, "Formatting table with metadata", ); // Format the table const table = this.formatRaw(headers, rows, options, context); return { table, metadata: { nullCounts: { ...this.nullCounts }, }, }; } /** * Format a raw 2D array with NULL value tracking. * Returns both the formatted table and metadata about NULL values. * * Note: For raw arrays, NULL tracking uses column indices as keys. * * @param headers - Array of column headers * @param rows - 2D array of cell values (may contain null/undefined) * @param options - Table formatting options * @param context - Optional request context for logging * @returns Object containing formatted table and metadata * @throws {McpError} If headers/rows are invalid or formatting fails * * @example * ```typescript * const headers = ['Name', 'Age', 'Salary']; * const rows = [ * ['Alice', 30, null], * ['Bob', 25, 50000] * ]; * const result = tableFormatter.formatRawWithMetadata(headers, rows); * console.log(result.metadata.nullCounts); // { '2': 1 } (column index 2) * ``` */ formatRawWithMetadata( headers: string[], rows: (string | null | undefined)[][], options?: TableFormatterOptions, context?: RequestContext, ): TableFormattingResult { const logContext = context || requestContextService.createRequestContext({ operation: "TableFormatter.formatRawWithMetadata", }); // Validate inputs if (!Array.isArray(headers) || headers.length === 0) { throw new McpError( JsonRpcErrorCode.ValidationError, "Headers must be a non-empty array", logContext, ); } if (!Array.isArray(rows)) { throw new McpError( JsonRpcErrorCode.ValidationError, "Rows must be an array", logContext, ); } if (rows.length === 0) { logger.debug(logContext, "Empty rows array provided to table formatter"); return { table: "", metadata: { nullCounts: {} }, }; } // Reset NULL tracking this.nullCounts = {}; // Merge options with defaults const opts: Required<TableFormatterOptions> = { ...this.defaultOptions, ...options, alignment: { ...this.defaultOptions.alignment, ...options?.alignment }, }; // Convert all cells to strings WITH NULL tracking const stringRows = rows.map((row) => row.map((cell, colIndex) => this.stringify(cell, opts.nullReplacement, colIndex.toString()), ), ); logger.debug( { ...logContext, rowCount: stringRows.length, columnCount: headers.length, nullCounts: this.nullCounts, }, "Formatting raw table with metadata", ); // Format the table const table = this.formatRaw(headers, stringRows, options, context); return { table, metadata: { nullCounts: { ...this.nullCounts }, }, }; } /** * Apply header styling transformations. * @private */ private applyHeaderStyle( headers: string[], style: "bold" | "uppercase" | "none", ): string[] { switch (style) { case "uppercase": return headers.map((h) => h.toUpperCase()); case "bold": case "none": default: return headers; } } /** * Calculate column widths and metadata. * @private */ private calculateColumns( headers: string[], rows: string[][], options: Required<TableFormatterOptions>, context: RequestContext, ): ColumnInfo[] { const columns: ColumnInfo[] = headers.map((header, index) => { // Determine alignment (use configured or default to left) const alignment = options.alignment[header] || options.alignment[index.toString()] || "left"; // Calculate max content width for this column const headerWidth = header.length; const maxContentWidth = Math.max( ...rows.map((row) => (row[index] || "").length), ); // Determine final width (respecting min/max constraints) let width = Math.max(headerWidth, maxContentWidth); width = Math.max(width, options.minWidth); if (options.maxWidth && width > options.maxWidth) { width = options.maxWidth; } return { name: header, width, alignment }; }); logger.debug( { ...context, columns: columns.map((c) => ({ name: c.name, width: c.width })), }, "Calculated column widths", ); return columns; } /** * Render table using the specified style. * @private */ private renderTable( columns: ColumnInfo[], headers: string[], rows: string[][], options: Required<TableFormatterOptions>, ): string { switch (options.style) { case "markdown": return this.renderMarkdown(columns, headers, rows, options); case "ascii": return this.renderAscii(columns, headers, rows, options); case "grid": return this.renderGrid(columns, headers, rows, options); case "compact": return this.renderCompact(columns, headers, rows, options); default: throw new Error(`Unknown table style: ${String(options.style)}`); } } /** * Render markdown-style table. * @private */ private renderMarkdown( columns: ColumnInfo[], headers: string[], rows: string[][], options: Required<TableFormatterOptions>, ): string { const lines: string[] = []; const pad = " ".repeat(options.padding); // Header row const headerCells = headers.map((header, i) => this.formatCell(header, columns[i]!, options), ); lines.push(`|${pad}${headerCells.join(`${pad}|${pad}`)}${pad}|`); // Separator row const separators = columns.map((col) => { const dashes = "-".repeat(col.width); return dashes; }); lines.push(`|${pad}${separators.join(`${pad}|${pad}`)}${pad}|`); // Data rows for (const row of rows) { const cells = row.map((cell, i) => this.formatCell(cell, columns[i]!, options), ); lines.push(`|${pad}${cells.join(`${pad}|${pad}`)}${pad}|`); } return lines.join("\n"); } /** * Render ASCII box-drawing table. * @private */ private renderAscii( columns: ColumnInfo[], headers: string[], rows: string[][], options: Required<TableFormatterOptions>, ): string { const lines: string[] = []; const pad = " ".repeat(options.padding); // Top border const topBorder = columns .map((col) => "-".repeat(col.width + options.padding * 2)) .join("+"); lines.push(`+${topBorder}+`); // Header row const headerCells = headers.map((header, i) => this.formatCell(header, columns[i]!, options), ); lines.push(`|${pad}${headerCells.join(`${pad}|${pad}`)}${pad}|`); // Header separator const headerSep = columns .map((col) => "-".repeat(col.width + options.padding * 2)) .join("+"); lines.push(`+${headerSep}+`); // Data rows for (const row of rows) { const cells = row.map((cell, i) => this.formatCell(cell, columns[i]!, options), ); lines.push(`|${pad}${cells.join(`${pad}|${pad}`)}${pad}|`); } // Bottom border const bottomBorder = columns .map((col) => "-".repeat(col.width + options.padding * 2)) .join("+"); lines.push(`+${bottomBorder}+`); return lines.join("\n"); } /** * Render Unicode grid table. * @private */ private renderGrid( columns: ColumnInfo[], headers: string[], rows: string[][], options: Required<TableFormatterOptions>, ): string { const lines: string[] = []; const pad = " ".repeat(options.padding); // Top border const topBorder = columns .map((col) => "─".repeat(col.width + options.padding * 2)) .join("┬"); lines.push(`┌${topBorder}┐`); // Header row const headerCells = headers.map((header, i) => this.formatCell(header, columns[i]!, options), ); lines.push(`│${pad}${headerCells.join(`${pad}│${pad}`)}${pad}│`); // Header separator const headerSep = columns .map((col) => "─".repeat(col.width + options.padding * 2)) .join("┼"); lines.push(`├${headerSep}┤`); // Data rows for (const row of rows) { const cells = row.map((cell, i) => this.formatCell(cell, columns[i]!, options), ); lines.push(`│${pad}${cells.join(`${pad}│${pad}`)}${pad}│`); } // Bottom border const bottomBorder = columns .map((col) => "─".repeat(col.width + options.padding * 2)) .join("┴"); lines.push(`└${bottomBorder}┘`); return lines.join("\n"); } /** * Render compact space-separated table. * @private */ private renderCompact( columns: ColumnInfo[], headers: string[], rows: string[][], options: Required<TableFormatterOptions>, ): string { const lines: string[] = []; const pad = " ".repeat(options.padding * 2); // Header row const headerCells = headers.map((header, i) => this.formatCell(header, columns[i]!, options), ); lines.push(headerCells.join(pad)); // Data rows for (const row of rows) { const cells = row.map((cell, i) => this.formatCell(cell, columns[i]!, options), ); lines.push(cells.join(pad)); } return lines.join("\n"); } /** * Format a single cell with alignment and truncation. * @private */ private formatCell( content: string, column: ColumnInfo, options: Required<TableFormatterOptions>, ): string { let text = content; // Truncate if needed if (options.truncate && text.length > column.width) { text = text.substring(0, column.width - 3) + "..."; } // Apply alignment padding const padding = column.width - text.length; if (padding <= 0) { return text; } switch (column.alignment) { case "left": return text + " ".repeat(padding); case "right": return " ".repeat(padding) + text; case "center": { const leftPad = Math.floor(padding / 2); const rightPad = padding - leftPad; return " ".repeat(leftPad) + text + " ".repeat(rightPad); } default: return text; } } /** * Convert any value to a string for display in table. * Tracks NULL values per column when columnKey is provided. * @private */ private stringify( value: unknown, nullReplacement: string = "-", columnKey?: string, ): string { // Handle NULL/undefined values with tracking if (value === null || value === undefined) { if (columnKey) { this.nullCounts[columnKey] = (this.nullCounts[columnKey] || 0) + 1; } return nullReplacement; } // Standard value conversion if (typeof value === "string") return value; if (typeof value === "number") return value.toString(); if (typeof value === "boolean") return value.toString(); if (typeof value === "bigint") return value.toString(); if (typeof value === "symbol") return value.toString(); if (typeof value === "function") return "[Function]"; if (Array.isArray(value)) return JSON.stringify(value); if (typeof value === "object") { try { return JSON.stringify(value); } catch { return "[Object]"; } } return "[Unknown]"; } } /** * Singleton instance of TableFormatter. * Use this instance to format tabular data into various table styles. * * @example * ```typescript * import { tableFormatter } from '@/utils/index.js'; * * const data = [ * { name: 'Alice', age: 30, role: 'Engineer' }, * { name: 'Bob', age: 25, role: 'Designer' } * ]; * * // Markdown table (default) * console.log(tableFormatter.format(data)); * * // Grid table with right-aligned age * console.log(tableFormatter.format(data, { * style: 'grid', * alignment: { age: 'right' } * })); * * // Compact table with uppercase headers * console.log(tableFormatter.format(data, { * style: 'compact', * headerStyle: 'uppercase' * })); * ``` */ export const tableFormatter = new TableFormatter();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/IBM/ibmi-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server