Files
dify/knowledge-fs/packages/database/scripts/migration-artifacts.ts
Jyong 4ee43b8afc chore: migrate knowledge-fs source tree
Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
2026-07-20 04:54:20 -04:00

104 lines
3.8 KiB
TypeScript

import { readFile, readdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
const mode = process.argv[2];
const root = resolve(import.meta.dirname, "../../..");
const migrationsDirectory = resolve(root, "packages/database/migrations");
const generatedRegistryPath = resolve(
root,
"packages/database/src/migration-artifacts.generated.ts",
);
if (mode !== "write" && mode !== "check") {
throw new Error("Usage: tsx packages/database/scripts/migration-artifacts.ts <write|check>");
}
const sourceArtifacts = await readMigrationSourceArtifacts();
const expectedRegistry = renderGeneratedRegistry(sourceArtifacts);
if (mode === "write") {
// SQL files are the immutable migration sources. Never regenerate or overwrite them from the
// current schema catalog; only refresh the registry embedded in the production bundle.
await writeFile(generatedRegistryPath, expectedRegistry, "utf8");
} else {
const currentRegistry = await readFile(generatedRegistryPath, "utf8").catch(() => undefined);
if (currentRegistry !== expectedRegistry) {
throw new Error("Database migration registry is out of date. Run pnpm db:migrations:write.");
}
}
interface SourceMigrationArtifact {
readonly content: string;
readonly filename: string;
readonly migrationId: string;
readonly path: string;
}
async function readMigrationSourceArtifacts(): Promise<readonly SourceMigrationArtifact[]> {
const filenames = (await readdir(migrationsDirectory))
.filter((filename) => filename.endsWith(".sql"))
.sort((left, right) => left.localeCompare(right));
const artifacts = await Promise.all(
filenames.map(async (filename): Promise<SourceMigrationArtifact> => {
const match = /^(\d{4}_[a-z0-9_]+)\.(postgres|tidb)\.sql$/u.exec(filename);
if (!match) {
throw new Error(`Invalid database migration filename: ${filename}`);
}
const migrationId = match[1];
const dialect = match[2];
if (!migrationId || !dialect) {
throw new Error(`Invalid database migration filename: ${filename}`);
}
const content = await readFile(resolve(migrationsDirectory, filename), "utf8");
if (!content.includes(`-- Migration id: ${migrationId}\n`)) {
throw new Error(`Migration ${filename} has a mismatched migration id header`);
}
if (!content.includes(`-- Dialect: ${dialect}\n`)) {
throw new Error(`Migration ${filename} has a mismatched dialect header`);
}
return {
content,
filename,
migrationId,
path: `packages/database/migrations/${filename}`,
};
}),
);
const dialectsByMigration = new Map<string, Set<string>>();
for (const artifact of artifacts) {
const dialect = artifact.filename.split(".").at(-2);
const dialects = dialectsByMigration.get(artifact.migrationId) ?? new Set<string>();
dialects.add(dialect ?? "");
dialectsByMigration.set(artifact.migrationId, dialects);
}
for (const [migrationId, dialects] of dialectsByMigration) {
if (!dialects.has("postgres") || !dialects.has("tidb") || dialects.size !== 2) {
throw new Error(`Migration ${migrationId} must provide exactly postgres and tidb artifacts`);
}
}
return artifacts;
}
function renderGeneratedRegistry(artifacts: readonly SourceMigrationArtifact[]): string {
const rows = artifacts.map(
(artifact) =>
` { content: ${JSON.stringify(artifact.content)}, path: ${JSON.stringify(artifact.path)} },`,
);
return [
"// Generated by packages/database/scripts/migration-artifacts.ts. Do not edit manually.",
'import type { MigrationArtifact } from "./migration-file";',
"",
"// biome-ignore format: one deterministic row per immutable SQL artifact keeps generation cheap.",
"export const migrationArtifacts = [",
...rows,
"] as const satisfies readonly MigrationArtifact[];",
"",
].join("\n");
}