@t1mmen/srtd
Version:
Supabase Repeatable Template Definitions (srtd): 🪄 Live-reloading SQL templates for Supabase DX. Make your database changes reviewable and migrations maintainable! 🚀
304 lines • 10.8 kB
JavaScript
/**
* Doctor checks - Pure functions for validating SRTD setup
* Each check returns a DoctorCheckResult with name, passed status, and optional message
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { CONFIG_FILE } from '../constants.js';
import { DatabaseService } from '../services/DatabaseService.js';
import { fileExists } from './fileExists.js';
import { validateBuildLog } from './schemas.js';
/**
* Check 1: Config file exists
*/
export async function checkConfigExists(projectRoot) {
const configPath = path.join(projectRoot, CONFIG_FILE);
const exists = await fileExists(configPath);
return {
name: 'Config file exists',
passed: exists,
message: exists ? undefined : `srtd.config.json not found in ${projectRoot}`,
hint: exists ? undefined : 'Run `srtd init` to create a config file',
};
}
/**
* Check 2: Config schema is valid
*/
export async function checkConfigSchemaValid(warnings) {
// Filter to only config-related warnings
const configWarnings = warnings.filter(w => w.source === 'config');
if (configWarnings.length === 0) {
return {
name: 'Config schema valid',
passed: true,
};
}
return {
name: 'Config schema valid',
passed: false,
message: configWarnings.map(w => w.message).join('; '),
hint: 'Check srtd.config.json for typos or invalid values',
};
}
/**
* Check 3: Template directory exists
*/
export async function checkTemplateDirExists(projectRoot, config) {
const templatePath = path.join(projectRoot, config.templateDir);
const exists = await fileExists(templatePath);
return {
name: 'Template directory exists',
passed: exists,
message: exists ? undefined : `Template directory not found: ${config.templateDir}`,
hint: exists ? undefined : `Create the directory: mkdir -p ${config.templateDir}`,
};
}
/**
* Check 4: Migration directory exists
*/
export async function checkMigrationDirExists(projectRoot, config) {
const migrationPath = path.join(projectRoot, config.migrationDir);
const exists = await fileExists(migrationPath);
return {
name: 'Migration directory exists',
passed: exists,
message: exists ? undefined : `Migration directory not found: ${config.migrationDir}`,
hint: exists ? undefined : `Create the directory: mkdir -p ${config.migrationDir}`,
};
}
/**
* Check 5: Template directory is readable
*/
export async function checkTemplateDirReadable(projectRoot, config) {
const templatePath = path.join(projectRoot, config.templateDir);
try {
await fs.readdir(templatePath);
return {
name: 'Template directory readable',
passed: true,
};
}
catch (error) {
return {
name: 'Template directory readable',
passed: false,
message: error instanceof Error ? error.message : 'Cannot read template directory',
hint: 'Check directory permissions or ownership',
};
}
}
/**
* Check 6: Migration directory is writable
*/
export async function checkMigrationDirWritable(projectRoot, config) {
const migrationPath = path.join(projectRoot, config.migrationDir);
const testFile = path.join(migrationPath, '.srtd-doctor-test');
let fileWritten = false;
try {
await fs.writeFile(testFile, 'test');
fileWritten = true;
return {
name: 'Migration directory writable',
passed: true,
};
}
catch (error) {
return {
name: 'Migration directory writable',
passed: false,
message: error instanceof Error ? error.message : 'Cannot write to migration directory',
hint: 'Check directory permissions or ownership',
};
}
finally {
// Cleanup test file if it was written (ignore cleanup errors)
if (fileWritten) {
await fs.unlink(testFile).catch(() => {
// Intentionally ignore - cleanup failure doesn't affect writability result
});
}
}
}
/**
* Check 7: Build log is valid (if exists)
*/
export async function checkBuildLogValid(projectRoot, config) {
const buildLogPath = path.join(projectRoot, config.buildLog);
try {
const content = await fs.readFile(buildLogPath, 'utf-8');
const result = validateBuildLog(content);
if (result.success) {
return {
name: 'Build log valid',
passed: true,
};
}
return {
name: 'Build log valid',
passed: false,
message: `Invalid build log: ${result.error}`,
hint: 'Delete the build log and run `srtd build` to regenerate it',
};
}
catch (error) {
// File not found is OK - build log is optional until first build
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
return {
name: 'Build log valid',
passed: true,
};
}
return {
name: 'Build log valid',
passed: false,
message: error instanceof Error ? error.message : 'Cannot read build log',
hint: 'Check file permissions or delete and regenerate with `srtd build`',
};
}
}
/**
* Check 8: Local build log is valid (if exists)
*/
export async function checkLocalBuildLogValid(projectRoot, config) {
const localBuildLogPath = path.join(projectRoot, config.localBuildLog);
try {
const content = await fs.readFile(localBuildLogPath, 'utf-8');
const result = validateBuildLog(content);
if (result.success) {
return {
name: 'Local build log valid',
passed: true,
};
}
return {
name: 'Local build log valid',
passed: false,
message: `Invalid local build log: ${result.error}`,
hint: 'Delete the local build log and run `srtd apply` to regenerate it',
};
}
catch (error) {
// File not found is OK - local build log is optional
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
return {
name: 'Local build log valid',
passed: true,
};
}
return {
name: 'Local build log valid',
passed: false,
message: error instanceof Error ? error.message : 'Cannot read local build log',
hint: 'Check file permissions or delete and regenerate with `srtd apply`',
};
}
}
/**
* Check 9: Database connection works
*
* Uses Promise.race for timeout. The dispose() method handles cleanup
* gracefully even if connection attempts are still in progress.
*/
export async function checkDatabaseConnection(config, timeoutMs = 5000) {
const db = DatabaseService.fromConfig(config);
let timeoutId;
try {
// Race between testConnection and timeout
// Capture timeout ID so we can clear it to prevent timer leaks
const timeoutPromise = new Promise(resolve => {
timeoutId = setTimeout(() => resolve('timeout'), timeoutMs);
});
const connectionPromise = db.testConnection();
const result = await Promise.race([connectionPromise, timeoutPromise]);
// Clear timeout immediately after race completes (whichever won)
if (timeoutId) {
clearTimeout(timeoutId);
timeoutId = undefined;
}
if (result === 'timeout') {
return {
name: 'Database connection',
passed: false,
message: `Connection timed out after ${timeoutMs}ms`,
hint: 'Check if database is running: `supabase status` or `docker ps`',
};
}
if (result === true) {
return {
name: 'Database connection',
passed: true,
};
}
return {
name: 'Database connection',
passed: false,
message: 'Connection failed. Check database server is running.',
hint: 'Run `supabase start` or check pgConnection in config',
};
}
catch (error) {
return {
name: 'Database connection',
passed: false,
message: error instanceof Error ? error.message : 'Connection failed',
hint: 'Run `supabase start` or verify pgConnection in srtd.config.json',
};
}
finally {
// Always clean up: clear timer and dispose database connection
if (timeoutId) {
clearTimeout(timeoutId);
}
await db.dispose();
}
}
/**
* Check 10: At least one SQL template exists
*/
export async function checkTemplateCount(projectRoot, config) {
const templatePath = path.join(projectRoot, config.templateDir);
try {
const files = await fs.readdir(templatePath);
const sqlFiles = files.filter(f => f.endsWith('.sql'));
if (sqlFiles.length >= 1) {
return {
name: 'Template count',
passed: true,
};
}
return {
name: 'Template count',
passed: false,
message: `No SQL templates found in ${config.templateDir}`,
hint: 'Add .sql template files or run `srtd init` to create examples',
};
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
return {
name: 'Template count',
passed: false,
message: `Cannot read template directory: ${errorMsg}`,
hint: 'Check directory permissions or ensure it exists',
};
}
}
/**
* Run all 10 doctor checks in order
*/
export async function runAllChecks(projectRoot, config, warnings) {
const results = [];
// Run checks in order
results.push(await checkConfigExists(projectRoot));
results.push(await checkConfigSchemaValid(warnings));
results.push(await checkTemplateDirExists(projectRoot, config));
results.push(await checkMigrationDirExists(projectRoot, config));
results.push(await checkTemplateDirReadable(projectRoot, config));
results.push(await checkMigrationDirWritable(projectRoot, config));
results.push(await checkBuildLogValid(projectRoot, config));
results.push(await checkLocalBuildLogValid(projectRoot, config));
results.push(await checkDatabaseConnection(config));
results.push(await checkTemplateCount(projectRoot, config));
return results;
}
//# sourceMappingURL=doctorChecks.js.map