migration.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. #!/usr/bin/env bun
  2. import { $ } from "bun"
  3. import fs from "fs/promises"
  4. import os from "os"
  5. import path from "path"
  6. import { pathToFileURL } from "url"
  7. const root = path.resolve(import.meta.dirname, "../../..")
  8. const sqlDir = path.join(root, "packages/core/migration")
  9. const tsDir = path.join(root, "packages/core/src/database/migration")
  10. const registry = path.join(root, "packages/core/src/database/migration.gen.ts")
  11. if (Bun.argv.includes("--check")) {
  12. await check()
  13. process.exit(0)
  14. }
  15. await $`bun drizzle-kit generate`.cwd(path.join(root, "packages/core"))
  16. const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir })))
  17. .map((file) => file.split("/")[0])
  18. .filter((name) => name !== undefined)
  19. .sort()
  20. for (const name of sqlMigrations) {
  21. if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
  22. await Bun.write(path.join(tsDir, `${name}.ts`), renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()))
  23. }
  24. await Bun.write(registry, renderRegistry(sqlMigrations))
  25. async function check() {
  26. const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-"))
  27. const output = path.join(temporary, "migration")
  28. try {
  29. await fs.cp(sqlDir, output, { recursive: true })
  30. const config = path.join(temporary, "drizzle.config.ts")
  31. await Bun.write(
  32. config,
  33. `import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)}
  34. export default { ...config, out: ${JSON.stringify(output)} }
  35. `,
  36. )
  37. const before = await snapshot(output)
  38. await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
  39. const after = await snapshot(output)
  40. if (JSON.stringify(after) !== JSON.stringify(before)) {
  41. throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.")
  42. }
  43. const migrations = before
  44. .map((entry) => entry.path.split("/")[0])
  45. .filter((name, index, all) => name !== undefined && all.indexOf(name) === index)
  46. .sort()
  47. for (const name of migrations) {
  48. if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
  49. throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`)
  50. }
  51. if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
  52. throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
  53. }
  54. } finally {
  55. await fs.rm(temporary, { recursive: true, force: true })
  56. }
  57. }
  58. async function snapshot(directory: string) {
  59. const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true }))
  60. return Promise.all(
  61. files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })),
  62. )
  63. }
  64. function renderMigration(name: string, sql: string) {
  65. return `import { Effect } from "effect"
  66. import type { DatabaseMigration } from "../migration"
  67. export default {
  68. id: ${JSON.stringify(name)},
  69. up(tx) {
  70. return Effect.gen(function* () {
  71. ${sql
  72. .split("--> statement-breakpoint")
  73. .map((statement) => statement.trim())
  74. .filter((statement) => statement.length > 0)
  75. .map(renderRun)
  76. .join("\n")}
  77. })
  78. },
  79. } satisfies DatabaseMigration.Migration
  80. `
  81. }
  82. function renderRun(statement: string) {
  83. const lines = statement.replaceAll("\t", " ").split("\n")
  84. if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)`
  85. return ` yield* tx.run(\`\n${lines.map((line) => ` ${escapeTemplate(line)}`).join("\n")}\n \`)`
  86. }
  87. function escapeTemplate(line: string) {
  88. return line.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")
  89. }
  90. function renderRegistry(names: string[]) {
  91. return `import type { DatabaseMigration } from "./migration"
  92. export const migrations = (await Promise.all([
  93. ${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
  94. ])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
  95. `
  96. }