migrations.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import logger from "@server/logger";
  2. import { __DIRNAME } from "@server/config";
  3. import { migrate } from "drizzle-orm/better-sqlite3/migrator";
  4. import db, { location } from "@server/db";
  5. import path from "path";
  6. import * as fs from "fs/promises";
  7. import semver from "semver";
  8. import { versionMigrations } from "@server/db/schema";
  9. import { desc, eq } from "drizzle-orm";
  10. export async function runMigrations() {
  11. if (!process.env.APP_VERSION) {
  12. throw new Error("APP_VERSION is not set in the environment");
  13. }
  14. if (process.env.ENVIRONMENT !== "prod") {
  15. logger.info("Skipping migrations in non-prod environment");
  16. return;
  17. }
  18. if (await checkFileExists(location)) {
  19. try {
  20. const directoryPath = path.join(__DIRNAME, "setup/scripts");
  21. // Get the last executed version from the database
  22. const lastExecuted = await db
  23. .select()
  24. .from(versionMigrations)
  25. .orderBy(desc(versionMigrations.version))
  26. .limit(1);
  27. // Use provided baseVersion or last executed version
  28. const startVersion = lastExecuted[0]?.version;
  29. // Read all files in directory
  30. const files = await fs.readdir(directoryPath);
  31. // Filter for .ts files and extract versions
  32. const versionedFiles = files
  33. .filter((file) => file.endsWith(".ts"))
  34. .map((file) => {
  35. const version = path.parse(file).name;
  36. return {
  37. version,
  38. path: path.join(directoryPath, file)
  39. };
  40. })
  41. .filter((file) => {
  42. // Validate that filename is a valid semver
  43. if (!semver.valid(file.version)) {
  44. console.warn(
  45. `Skipping invalid semver filename: ${file.path}`
  46. );
  47. return false;
  48. }
  49. // Filter versions based on startVersion if provided
  50. if (startVersion) {
  51. return semver.gt(file.version, startVersion);
  52. }
  53. return true;
  54. });
  55. // Sort files by semver
  56. const sortedFiles = versionedFiles.sort((a, b) =>
  57. semver.compare(a.version, b.version)
  58. );
  59. const results: FileExecutionResult[] = [];
  60. // Execute files in order
  61. for (const file of sortedFiles) {
  62. try {
  63. // Start a transaction for each file execution
  64. await db.transaction(async (tx) => {
  65. // Check if version was already executed (double-check within transaction)
  66. const executed = await tx
  67. .select()
  68. .from(versionMigrations)
  69. .where(eq(versionMigrations.version, file.version));
  70. if (executed.length > 0) {
  71. throw new Error(
  72. `Version ${file.version} was already executed`
  73. );
  74. }
  75. // Dynamic import of the TypeScript file
  76. const module = await import(file.path);
  77. // Execute default export if it's a function
  78. if (typeof module.default === "function") {
  79. await module.default();
  80. } else {
  81. throw new Error(
  82. `No default export function in ${file.path}`
  83. );
  84. }
  85. // Record successful execution
  86. const executedAt = Date.now();
  87. await tx.insert(versionMigrations).values({
  88. version: file.version,
  89. executedAt: executedAt
  90. });
  91. results.push({
  92. version: file.version,
  93. success: true,
  94. executedAt
  95. });
  96. });
  97. } catch (error) {
  98. const executedAt = Date.now();
  99. results.push({
  100. version: file.version,
  101. success: false,
  102. executedAt,
  103. error:
  104. error instanceof Error
  105. ? error
  106. : new Error(String(error))
  107. });
  108. // Log error but continue processing other files
  109. console.error(`Error executing ${file.path}:`, error);
  110. }
  111. }
  112. return results;
  113. } catch (error) {
  114. throw new Error(`Failed to process directory: ${error}`);
  115. }
  116. } else {
  117. logger.info("Running migrations...");
  118. try {
  119. migrate(db, {
  120. migrationsFolder: path.join(__DIRNAME, "init")
  121. });
  122. logger.info("Migrations completed successfully.");
  123. } catch (error) {
  124. logger.error("Error running migrations:", error);
  125. }
  126. // insert process.env.APP_VERSION into the versionMigrations table
  127. await db
  128. .insert(versionMigrations)
  129. .values({
  130. version: process.env.APP_VERSION,
  131. executedAt: Date.now()
  132. })
  133. .execute();
  134. }
  135. }
  136. async function checkFileExists(filePath: string): Promise<boolean> {
  137. try {
  138. await fs.access(filePath);
  139. return true;
  140. } catch {
  141. return false;
  142. }
  143. }
  144. interface FileExecutionResult {
  145. version: string;
  146. success: boolean;
  147. executedAt: number;
  148. error?: Error;
  149. }