everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
1 lines • 8.41 kB
Source Map (JSON)
{"version":3,"file":"db-doctor.cjs","names":["pluginMigrationSlug","extractExpectedTables"],"sources":["../../src/cli/db-doctor.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { extractExpectedTables, pluginMigrationSlug } from \"../db\";\nimport type { PluginDbInfo } from \"./db-studio\";\n\nexport interface DoctorReport {\n plugin: string;\n slug: string;\n journalTable: string;\n journalSchema: string;\n dbSecret: string;\n dbUrl: string;\n workspaceDir: string | undefined;\n localMigrationCount: number;\n appliedHashCount: number;\n expectedTables: string[];\n missingTables: string[];\n migrationHashes: string[];\n diagnosis: string;\n legacyCount: number;\n error?: string;\n}\n\nfunction hashFile(raw: string): string {\n return createHash(\"sha256\").update(raw).digest(\"hex\");\n}\n\ninterface LocalMigration {\n tag: string;\n hash: string;\n sql: string[];\n}\n\nfunction readLocalMigrations(workspaceDir: string): LocalMigration[] {\n const migrationsDir = resolve(workspaceDir, \"src/db/migrations\");\n const metaDir = join(migrationsDir, \"meta\");\n const journalPath = join(metaDir, \"_journal.json\");\n if (!existsSync(journalPath)) return [];\n\n const journal = JSON.parse(readFileSync(journalPath, \"utf8\")) as {\n entries: { idx: number; tag: string }[];\n };\n\n return journal.entries.map((entry) => {\n const sqlPath = join(migrationsDir, `${entry.tag}.sql`);\n if (!existsSync(sqlPath)) {\n return { tag: entry.tag, hash: \"\", sql: [] };\n }\n const raw = readFileSync(sqlPath, \"utf8\");\n const sql = raw.split(\"--> statement-breakpoint\").map((s) => s.trim());\n return { tag: entry.tag, hash: hashFile(raw), sql };\n });\n}\n\nexport async function diagnosePlugin(info: PluginDbInfo): Promise<DoctorReport> {\n const { Pool } = await import(\"pg\");\n\n const slug = pluginMigrationSlug(info.key);\n const journalTable = `__drizzle_migrations_${slug}`;\n const journalSchema = \"drizzle\";\n const journalRef = `\"${journalSchema}\".\"${journalTable}\"`;\n\n const localMigrations = info.workspaceDir ? readLocalMigrations(info.workspaceDir) : [];\n\n const expectedTables = extractExpectedTables(localMigrations);\n const localHashes = localMigrations.map((m) => m.hash).filter(Boolean);\n const migrationHashes = localHashes;\n\n const pool = new Pool({\n connectionString: info.databaseUrl,\n ssl:\n info.databaseUrl.includes(\"localhost\") || info.databaseUrl.includes(\"127.0.0.1\")\n ? false\n : { rejectUnauthorized: false },\n max: 1,\n connectionTimeoutMillis: 10_000,\n });\n\n try {\n const journalExists = await pool.query(`\n SELECT EXISTS (\n SELECT 1 FROM information_schema.tables\n WHERE table_schema = '${journalSchema}' AND table_name = '${journalTable}'\n ) AS exists\n `);\n\n let appliedHashCount = 0;\n if (journalExists.rows[0]?.exists) {\n const result = await pool.query(`SELECT hash FROM ${journalRef}`);\n appliedHashCount = result.rows.length;\n }\n\n let missingTables: string[] = [];\n if (expectedTables.length > 0) {\n const tableResult = await pool.query(\n `\n SELECT table_name FROM information_schema.tables\n WHERE table_schema = 'public'\n AND table_name = ANY($1)\n `,\n [expectedTables],\n );\n const existing = new Set(tableResult.rows.map((r: any) => r.table_name));\n missingTables = expectedTables.filter((t) => !existing.has(t));\n }\n\n const legacyCount = await countLegacyHashes(pool, localHashes);\n\n let diagnosis: string;\n if (localMigrations.length === 0) {\n diagnosis = \"no-local-migrations\";\n } else if (appliedHashCount === 0 && localHashes.length > 0) {\n diagnosis = legacyCount > 0 ? \"legacy-importable\" : \"unapplied\";\n } else if (missingTables.length === 0) {\n diagnosis = \"healthy\";\n } else if (missingTables.length === expectedTables.length) {\n diagnosis = \"drift-safe-repair\";\n } else {\n diagnosis = \"drift-manual\";\n }\n\n const masked = info.databaseUrl.replace(/\\/\\/[^:]+:[^@]+@/, \"//***:***@\");\n\n return {\n plugin: info.key,\n slug,\n journalTable,\n journalSchema,\n dbSecret: info.databaseSecret,\n dbUrl: masked,\n workspaceDir: info.workspaceDir,\n localMigrationCount: localMigrations.length,\n appliedHashCount,\n expectedTables,\n missingTables,\n migrationHashes,\n diagnosis,\n legacyCount,\n };\n } catch (error) {\n return {\n plugin: info.key,\n slug,\n journalTable,\n journalSchema,\n dbSecret: info.databaseSecret,\n dbUrl: info.databaseUrl.replace(/\\/\\/[^:]+:[^@]+@/, \"//***:***@\"),\n workspaceDir: info.workspaceDir,\n localMigrationCount: localMigrations.length,\n appliedHashCount: 0,\n expectedTables,\n missingTables: [],\n migrationHashes: [],\n diagnosis: \"error\",\n legacyCount: 0,\n error: error instanceof Error ? error.message : String(error),\n };\n } finally {\n await pool.end().catch(() => {});\n }\n}\n\nasync function countLegacyHashes(pool: any, localHashes: string[]): Promise<number> {\n if (localHashes.length === 0) return 0;\n const candidates = [\n { schema: \"drizzle\", table: \"__drizzle_migrations\" },\n { schema: \"public\", table: \"drizzle_migrations\" },\n ];\n let total = 0;\n for (const c of candidates) {\n try {\n const result = await pool.query(\n `SELECT count(*)::int AS cnt FROM \"${c.schema}\".\"${c.table}\" WHERE hash = ANY($1)`,\n [localHashes],\n );\n total += result.rows[0]?.cnt ?? 0;\n } catch {\n // table might not exist\n }\n }\n return total;\n}\n"],"mappings":"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"}