UNPKG

@t1mmen/srtd

Version:

Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀

243 lines 8.71 kB
import chalk from 'chalk'; import figures from 'figures'; import { formatPath } from '../utils/formatPath.js'; import { formatTime } from '../utils/formatTime.js'; import { TIMESTAMP_COLUMN_WIDTH } from './constants.js'; import { renderErrorContext } from './errorContext.js'; const COL_TEMPLATE = 22; const COL_TARGET = 32; /** * Get status label for watch mode display. */ function getStatusLabel(status) { switch (status) { case 'success': return 'applied'; case 'built': return 'built'; case 'changed': return 'changed'; case 'error': return 'error'; case 'unchanged': return 'unchanged'; case 'skipped': return 'skipped'; } } /** * Get icon for a template result status. */ function getStatusIcon(status) { switch (status) { case 'success': case 'built': return chalk.green(figures.tick); case 'changed': return chalk.dim(figures.bullet); case 'unchanged': return chalk.dim(figures.bullet); case 'skipped': return chalk.yellow(figures.arrowRight); case 'error': return chalk.red(figures.cross); } } /** * Get color for a status. */ function getStatusColor(status) { switch (status) { case 'success': case 'built': return chalk.green; case 'changed': return chalk.dim; case 'unchanged': return chalk.dim; case 'skipped': return chalk.yellow; case 'error': return chalk.red; } } /** * Get target display text for a result. * - Build: migration filename * - Apply: "local db" */ function getTargetDisplay(result, context) { if (result.status === 'error') return ''; if (context.command === 'apply' || context.command === 'watch') return 'local db'; return result.target || ''; } /** * Check if a timestamp is recent (within the last minute). */ function isRecent(timestamp) { if (!timestamp) return false; const ONE_MINUTE = 60 * 1000; return Date.now() - timestamp.getTime() < ONE_MINUTE; } /** * Render a single result row for watch mode (streaming log format). * Format: HH:MM:SS ✔ template.sql applied * * Color semantics: * - Recent success/built (< 1min): GREEN (just acted on) * - Old success/built (> 1min): DIM (historic, no longer "fresh") * - unchanged: DIM (no action) * - changed: DIM icon, normal text (pending action) * - error: RED (problem) */ function renderWatchRow(result) { const time = result.timestamp ? formatTime.time(result.timestamp) : ''; const truncatedPath = formatPath.truncatePath(result.template); // For success/built, check if recent - old entries should be dim const recent = isRecent(result.timestamp); const isSuccessType = result.status === 'success' || result.status === 'built'; // Determine effective color based on recency const color = isSuccessType && !recent ? chalk.dim : getStatusColor(result.status); const icon = isSuccessType && !recent ? chalk.dim(figures.tick) : getStatusIcon(result.status); // Use displayOverride if provided, otherwise color the label let statusLabel = result.displayOverride || color(getStatusLabel(result.status)); // Add build outdated annotation for changed status if (result.status === 'changed' && result.buildOutdated) { statusLabel += chalk.yellow(' (build outdated)'); } // Add arrow for built status with target if (result.status === 'built' && result.target) { statusLabel += ` ${chalk.dim('→')} ${color(result.target)}`; } // Build the main line: "16:45:02 ✓ .../file.sql applied" const mainLine = `${chalk.dim(time)} ${icon} ${color(truncatedPath)} ${statusLabel}`; console.log(mainLine); // For errors, render additional context if (result.status === 'error') { const indent = ' '.repeat(TIMESTAMP_COLUMN_WIDTH); renderErrorContext({ message: result.errorMessage, hint: result.errorHint, sqlSnippet: result.errorSqlSnippet, column: result.errorColumn, indentPrefix: indent, }); } } /** * Render a single result row for build/apply mode (table format). * Format: ✔ template.sql → target * * Color semantics: * - success/built: GREEN (just acted on) * - unchanged/skipped: DIM (no action taken) * - error: RED (problem) */ function renderTableRow(result, context) { const icon = getStatusIcon(result.status); const color = getStatusColor(result.status); const templateName = formatPath.ensureSqlExtension(result.template); // Apply consistent coloring to template name based on status const templateDisplay = color(templateName.padEnd(COL_TEMPLATE)); // For errors, don't show arrow (nothing was created/applied) if (result.status === 'error') { console.log(`${icon} ${templateDisplay}`); return; } // For skipped (WIP templates), show hint to promote if (result.status === 'skipped') { console.log(`${icon} ${templateDisplay} ${chalk.yellow('(wip)')} ${chalk.dim.italic('promote to build')}`); return; } const arrow = chalk.dim('→'); const targetText = getTargetDisplay(result, context); if (result.status === 'unchanged') { // Unchanged: dim everything including target and time const targetDisplay = chalk.dim(targetText.padEnd(COL_TARGET)); const timeDisplay = result.timestamp ? chalk.dim(formatTime.relative(result.timestamp)) : ''; console.log(`${icon} ${templateDisplay} ${arrow} ${targetDisplay} ${timeDisplay}`); } else { // Success/built: color the target to match (green = just acted on) console.log(`${icon} ${templateDisplay} ${arrow} ${color(targetText)}`); } } /** * Render a single result row - dispatches to appropriate renderer based on context. */ export function renderResultRow(result, context) { if (context.command === 'watch') { renderWatchRow(result); } else { renderTableRow(result, context); } } /** * Render summary footer with consistent icon format. * Pattern: [icon] [message] * * Color semantics: summary uses same colors as results * - Success/built count: GREEN * - Error count: RED * - No changes: DIM */ function renderSummary(results, context) { // No summary for watch mode - it's streaming if (context.command === 'watch') return; // Count both 'success' (applied) and 'built' as successful actions const successCount = results.filter(r => r.status === 'success' || r.status === 'built').length; const errorCount = results.filter(r => r.status === 'error').length; const unchangedCount = results.filter(r => r.status === 'unchanged').length; console.log(); // Show success count if any if (successCount > 0) { const verb = context.command === 'build' ? 'Built' : 'Applied'; console.log(chalk.green(`${figures.tick} ${verb} ${successCount} template${successCount !== 1 ? 's' : ''}`)); } // Show error count if any if (errorCount > 0) { console.log(chalk.red(`${figures.cross} ${errorCount} error${errorCount !== 1 ? 's' : ''}`)); } // If nothing happened (only unchanged), show "No changes" if (successCount === 0 && errorCount === 0 && unchangedCount > 0) { console.log(chalk.dim(`${figures.bullet} No changes`)); } } /** * Render results as an aligned table with arrow format showing targets. * * Build/Apply Format: * ✔ audit.sql → 20241227_srtd-audit.sql * ● users.sql → 20241225_srtd-users.sql 2 days ago * ✘ broken.sql * * Watch Format: * 16:45:02 ✔ .../audit.sql applied * 16:45:15 ● .../user.sql changed * 16:46:03 ✘ .../broken.sql error * | syntax error at line 5 */ export function renderResultsTable(options) { const { results, context } = options; // For build command, sort skipped (WIP) templates to bottom // This keeps focus on what will actually be built const sortedResults = context.command === 'build' ? [...results].sort((a, b) => { if (a.status === 'skipped' && b.status !== 'skipped') return 1; if (a.status !== 'skipped' && b.status === 'skipped') return -1; return 0; }) : results; for (const result of sortedResults) { renderResultRow(result, context); } renderSummary(results, context); } //# sourceMappingURL=resultsTable.js.map