114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { $ } from "bun"
|
|
import fs from "fs/promises"
|
|
import os from "os"
|
|
import path from "path"
|
|
import { pathToFileURL } from "url"
|
|
|
|
const root = path.resolve(import.meta.dirname, "../../..")
|
|
const sqlDir = path.join(root, "packages/core/migration")
|
|
const tsDir = path.join(root, "packages/core/src/database/migration")
|
|
const registry = path.join(root, "packages/core/src/database/migration.gen.ts")
|
|
|
|
if (Bun.argv.includes("--check")) {
|
|
await check()
|
|
process.exit(0)
|
|
}
|
|
|
|
await $`bun drizzle-kit generate`.cwd(path.join(root, "packages/core"))
|
|
|
|
const sqlMigrations = (await Array.fromAsync(new Bun.Glob("*/migration.sql").scan({ cwd: sqlDir })))
|
|
.map((file) => file.split("/")[0])
|
|
.filter((name) => name !== undefined)
|
|
.sort()
|
|
|
|
for (const name of sqlMigrations) {
|
|
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
|
await Bun.write(path.join(tsDir, `${name}.ts`), renderMigration(name, await Bun.file(path.join(sqlDir, name, "migration.sql")).text()))
|
|
}
|
|
|
|
await Bun.write(registry, renderRegistry(sqlMigrations))
|
|
|
|
async function check() {
|
|
const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-core-migration-check-"))
|
|
const output = path.join(temporary, "migration")
|
|
try {
|
|
await fs.cp(sqlDir, output, { recursive: true })
|
|
const config = path.join(temporary, "drizzle.config.ts")
|
|
await Bun.write(
|
|
config,
|
|
`import config from ${JSON.stringify(pathToFileURL(path.join(root, "packages/core/drizzle.config.ts")).href)}
|
|
|
|
export default { ...config, out: ${JSON.stringify(output)} }
|
|
`,
|
|
)
|
|
const before = await snapshot(output)
|
|
await $`bun drizzle-kit generate --config ${config}`.cwd(path.join(root, "packages/core"))
|
|
const after = await snapshot(output)
|
|
if (JSON.stringify(after) !== JSON.stringify(before)) {
|
|
throw new Error("Core schema has ungenerated database migrations. Run `bun script/migration.ts` from packages/core.")
|
|
}
|
|
|
|
const migrations = before
|
|
.map((entry) => entry.path.split("/")[0])
|
|
.filter((name, index, all) => name !== undefined && all.indexOf(name) === index)
|
|
.sort()
|
|
for (const name of migrations) {
|
|
if (await Bun.file(path.join(tsDir, `${name}.ts`)).exists()) continue
|
|
throw new Error(`Database migration TypeScript wrapper is missing for ${name}. Run \`bun script/migration.ts\` from packages/core.`)
|
|
}
|
|
if ((await Bun.file(registry).text()) !== renderRegistry(migrations)) {
|
|
throw new Error("Database migration registry is stale. Run `bun script/migration.ts` from packages/core.")
|
|
}
|
|
} finally {
|
|
await fs.rm(temporary, { recursive: true, force: true })
|
|
}
|
|
}
|
|
|
|
async function snapshot(directory: string) {
|
|
const files = await Array.fromAsync(new Bun.Glob("**/*").scan({ cwd: directory, onlyFiles: true }))
|
|
return Promise.all(
|
|
files.sort().map(async (file) => ({ path: file, contents: await Bun.file(path.join(directory, file)).text() })),
|
|
)
|
|
}
|
|
|
|
function renderMigration(name: string, sql: string) {
|
|
return `import { Effect } from "effect"
|
|
import type { DatabaseMigration } from "../migration"
|
|
|
|
export default {
|
|
id: ${JSON.stringify(name)},
|
|
up(tx) {
|
|
return Effect.gen(function* () {
|
|
${sql
|
|
.split("--> statement-breakpoint")
|
|
.map((statement) => statement.trim())
|
|
.filter((statement) => statement.length > 0)
|
|
.map(renderRun)
|
|
.join("\n")}
|
|
})
|
|
},
|
|
} satisfies DatabaseMigration.Migration
|
|
`
|
|
}
|
|
|
|
function renderRun(statement: string) {
|
|
const lines = statement.replaceAll("\t", " ").split("\n")
|
|
if (lines.length === 1) return ` yield* tx.run(\`${escapeTemplate(lines[0])}\`)`
|
|
return ` yield* tx.run(\`\n${lines.map((line) => ` ${escapeTemplate(line)}`).join("\n")}\n \`)`
|
|
}
|
|
|
|
function escapeTemplate(line: string) {
|
|
return line.replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("${", "\\${")
|
|
}
|
|
|
|
function renderRegistry(names: string[]) {
|
|
return `import type { DatabaseMigration } from "./migration"
|
|
|
|
export const migrations = (await Promise.all([
|
|
${names.map((name) => ` import("./migration/${name}"),`).join("\n")}
|
|
])).map((module) => module.default) satisfies DatabaseMigration.Migration[]
|
|
`
|
|
}
|