@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
JavaScript
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