UNPKG

longcelot-sheet-db

Version:

Google Sheets-backed staging database adapter for Node.js with schema-first design

162 lines 6.33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DEFAULT_ER_DIAGRAM_FILENAME = void 0; exports.generateMermaidERDiagram = generateMermaidERDiagram; exports.erdiagramCommand = erdiagramCommand; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const chalk_1 = __importDefault(require("chalk")); const inquirer_1 = __importDefault(require("inquirer")); const adminAdapter_1 = require("../lib/adminAdapter"); const schemaLoader_1 = require("../lib/schemaLoader"); exports.DEFAULT_ER_DIAGRAM_FILENAME = 'ER-DIAGRAM.md'; function keyMarker(colName, col, pkColumn) { if (colName === pkColumn) return 'PK'; if (col.ref) return 'FK'; if (col.unique) return 'UK'; return ''; } /** * Renders `erDiagram` body only (no ```mermaid fences) so it can be embedded in a * Markdown file or tested directly against its text output. */ function generateMermaidERDiagram(schemas) { const knownTables = new Set(schemas.map((s) => s.name)); const lines = ['erDiagram']; for (const schema of schemas) { const pkColumn = schema.pkColumn ?? '_id'; lines.push(` ${schema.name} {`); for (const [colName, col] of Object.entries(schema.columns)) { const marker = keyMarker(colName, col, pkColumn); const parts = [col.type, colName]; if (marker) parts.push(marker); lines.push(` ${parts.join(' ')}`); } lines.push(' }'); } const relationLines = []; for (const schema of schemas) { for (const [colName, col] of Object.entries(schema.columns)) { if (!col.ref) continue; const [refTable] = col.ref.split('.'); if (!knownTables.has(refTable)) continue; // dangling ref() — not registered on this adapter const cardinality = col.unique ? '||--||' : '||--o{'; relationLines.push(` ${refTable} ${cardinality} ${schema.name} : "${colName}"`); } } if (relationLines.length > 0) { lines.push(''); lines.push(...relationLines); } return lines.join('\n'); } function buildMarkdown(schemas) { const diagram = generateMermaidERDiagram(schemas); const byActor = new Map(); for (const schema of schemas) { const list = byActor.get(schema.actor) ?? []; list.push(schema.name); byActor.set(schema.actor, list); } const tableList = [...byActor.entries()] .map(([actor, tables]) => `- **${actor}**: ${tables.sort().join(', ')}`) .join('\n'); return [ '# ER Diagram', '', `Auto-generated by \`lsdb erdiagram\` on ${new Date().toISOString()}. Re-run after schema changes to refresh.`, '', '## Tables by actor', '', tableList, '', '## Diagram', '', '```mermaid', diagram, '```', '', ].join('\n'); } /** Suggests `name-1.ext`, bumping an existing trailing `-N` to `-(N+1)`. */ function nextCandidateName(filename) { const ext = path_1.default.extname(filename); const base = path_1.default.basename(filename, ext); const match = base.match(/^(.*)-(\d+)$/); if (match) { return `${match[1]}-${parseInt(match[2], 10) + 1}${ext}`; } return `${base}-1${ext}`; } /** * Resolves the final write path, prompting the developer when the target already exists: * overwrite it, save under a different name (looped until a free/confirmed path is chosen), * or cancel. Returns null on cancel. `yes` bypasses the prompt and always overwrites. */ async function resolveOutputPath(initialPath, yes) { let candidate = initialPath; while (fs_1.default.existsSync(candidate)) { if (yes) return candidate; const { action } = await inquirer_1.default.prompt([ { type: 'list', name: 'action', message: `${path_1.default.basename(candidate)} already exists. What would you like to do?`, choices: [ { name: 'Overwrite existing file', value: 'overwrite' }, { name: 'Save as a different file name', value: 'rename' }, { name: 'Cancel', value: 'cancel' }, ], }, ]); if (action === 'overwrite') return candidate; if (action === 'cancel') return null; const { newName } = await inquirer_1.default.prompt([ { type: 'input', name: 'newName', message: 'Enter a new file name:', default: nextCandidateName(path_1.default.basename(candidate)), validate: (input) => (input.trim().length > 0 ? true : 'File name cannot be empty'), }, ]); candidate = path_1.default.join(path_1.default.dirname(candidate), newName.trim()); } return candidate; } async function erdiagramCommand(options = {}) { console.log(chalk_1.default.blue.bold('🗺️ Generating ER diagram...\n')); const config = (0, adminAdapter_1.loadCLIConfig)(); const loaded = (0, schemaLoader_1.loadSchemasWithPaths)(config); if (loaded.length === 0) { console.log(chalk_1.default.yellow('⚠️ No schemas found')); return; } const schemas = loaded.map((l) => l.schema); const markdown = buildMarkdown(schemas); const requestedPath = options.output ? path_1.default.resolve(process.cwd(), options.output) : path_1.default.join(process.cwd(), exports.DEFAULT_ER_DIAGRAM_FILENAME); const outPath = await resolveOutputPath(requestedPath, options.yes ?? false); if (!outPath) { console.log(chalk_1.default.gray('Cancelled.')); return; } fs_1.default.writeFileSync(outPath, markdown); console.log(chalk_1.default.green(`✅ ER diagram written to ${outPath}`)); console.log(chalk_1.default.cyan(`\n${schemas.length} table(s) diagrammed`)); } exports.default = erdiagramCommand; //# sourceMappingURL=erdiagram.js.map