소스 검색

Complete migration setup

Owen Schwartz 7 달 전
부모
커밋
993eab5ac1
5개의 변경된 파일79개의 추가작업 그리고 129개의 파일을 삭제
  1. 1 1
      Dockerfile
  2. 11 0
      server/db/index.ts
  3. 2 0
      server/db/schema.ts
  4. 63 127
      server/setup/migrations.ts
  5. 2 1
      server/setup/scripts/1.0.0.ts

+ 1 - 1
Dockerfile

@@ -24,7 +24,7 @@ RUN npm install --omit=dev --legacy-peer-deps
 
 COPY --from=builder /app/.next ./.next
 COPY --from=builder /app/dist ./dist
-COPY --from=builder /app/migrations ./dist/migrations
+COPY --from=builder /app/init ./dist/init
 
 COPY config.example.yml ./dist/config.example.yml
 COPY server/db/names.json ./dist/names.json

+ 11 - 0
server/db/index.ts

@@ -3,10 +3,21 @@ import Database from "better-sqlite3";
 import * as schema from "@server/db/schema";
 import { APP_PATH } from "@server/config";
 import path from "path";
+import fs from "fs/promises";
 
 export const location = path.join(APP_PATH, "db", "db.sqlite");
+export const exists = await checkFileExists(location);
 
 const sqlite = new Database(location);
 export const db = drizzle(sqlite, { schema });
 
 export default db;
+
+async function checkFileExists(filePath: string): Promise<boolean> {
+    try {
+        await fs.access(filePath);
+        return true;
+    } catch {
+        return false;
+    }
+}

+ 2 - 0
server/db/schema.ts

@@ -394,3 +394,5 @@ export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
 export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
 export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
 export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
+export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
+export type VersionMigration = InferSelectModel<typeof versionMigrations>;

+ 63 - 127
server/setup/migrations.ts

@@ -1,12 +1,21 @@
 import logger from "@server/logger";
 import { __DIRNAME } from "@server/config";
 import { migrate } from "drizzle-orm/better-sqlite3/migrator";
-import db, { location } from "@server/db";
+import db, { exists } from "@server/db";
 import path from "path";
-import * as fs from "fs/promises";
 import semver from "semver";
 import { versionMigrations } from "@server/db/schema";
-import { desc, eq } from "drizzle-orm";
+import { desc } from "drizzle-orm";
+
+// Import all migrations explicitly
+import migration100 from "./scripts/1.0.0";
+// Add new migration imports here as they are created
+
+// Define the migration list with versions and their corresponding functions
+const migrations = [
+    { version: "1.0.0", run: migration100 }
+    // Add new migrations here as they are created
+] as const;
 
 export async function runMigrations() {
     if (!process.env.APP_VERSION) {
@@ -18,122 +27,13 @@ export async function runMigrations() {
         return;
     }
 
-    if (await checkFileExists(location)) {
-        try {
-            const directoryPath = path.join(__DIRNAME, "setup/scripts");
-            // Get the last executed version from the database
-            const lastExecuted = await db
-                .select()
-                .from(versionMigrations)
-                .orderBy(desc(versionMigrations.version))
-                .limit(1);
-
-            // Use provided baseVersion or last executed version
-            const startVersion = lastExecuted[0]?.version;
-
-            // Read all files in directory
-            const files = await fs.readdir(directoryPath);
-
-            // Filter for .ts files and extract versions
-            const versionedFiles = files
-                .filter((file) => file.endsWith(".ts"))
-                .map((file) => {
-                    const version = path.parse(file).name;
-                    return {
-                        version,
-                        path: path.join(directoryPath, file)
-                    };
-                })
-                .filter((file) => {
-                    // Validate that filename is a valid semver
-                    if (!semver.valid(file.version)) {
-                        console.warn(
-                            `Skipping invalid semver filename: ${file.path}`
-                        );
-                        return false;
-                    }
-                    // Filter versions based on startVersion if provided
-                    if (startVersion) {
-                        return semver.gt(file.version, startVersion);
-                    }
-                    return true;
-                });
-
-            // Sort files by semver
-            const sortedFiles = versionedFiles.sort((a, b) =>
-                semver.compare(a.version, b.version)
-            );
-
-            const results: FileExecutionResult[] = [];
-
-            // Execute files in order
-            for (const file of sortedFiles) {
-                try {
-                    // Start a transaction for each file execution
-                    await db.transaction(async (tx) => {
-                        // Check if version was already executed (double-check within transaction)
-                        const executed = await tx
-                            .select()
-                            .from(versionMigrations)
-                            .where(eq(versionMigrations.version, file.version));
-
-                        if (executed.length > 0) {
-                            throw new Error(
-                                `Version ${file.version} was already executed`
-                            );
-                        }
-
-                        // Dynamic import of the TypeScript file
-                        const module = await import(file.path);
-
-                        // Execute default export if it's a function
-                        if (typeof module.default === "function") {
-                            await module.default();
-                        } else {
-                            throw new Error(
-                                `No default export function in ${file.path}`
-                            );
-                        }
-
-                        // Record successful execution
-                        const executedAt = Date.now();
-                        await tx.insert(versionMigrations).values({
-                            version: file.version,
-                            executedAt: executedAt
-                        });
-
-                        results.push({
-                            version: file.version,
-                            success: true,
-                            executedAt
-                        });
-                    });
-                } catch (error) {
-                    const executedAt = Date.now();
-                    results.push({
-                        version: file.version,
-                        success: false,
-                        executedAt,
-                        error:
-                            error instanceof Error
-                                ? error
-                                : new Error(String(error))
-                    });
-
-                    // Log error but continue processing other files
-                    console.error(`Error executing ${file.path}:`, error);
-                }
-            }
-
-            return results;
-        } catch (error) {
-            throw new Error(`Failed to process directory: ${error}`);
-        }
+    if (exists) {
+        await executeScripts();
     } else {
         logger.info("Running migrations...");
         try {
             migrate(db, {
-                migrationsFolder: path.join(__DIRNAME, "init")
+                migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
             });
             logger.info("Migrations completed successfully.");
         } catch (error) {
@@ -151,18 +51,54 @@ export async function runMigrations() {
     }
 }
 
-async function checkFileExists(filePath: string): Promise<boolean> {
+async function executeScripts() {
     try {
-        await fs.access(filePath);
-        return true;
-    } catch {
-        return false;
-    }
-}
+        // Get the last executed version from the database
+        const lastExecuted = await db
+            .select()
+            .from(versionMigrations)
+            .orderBy(desc(versionMigrations.version))
+            .limit(1);
+
+        const startVersion = lastExecuted[0]?.version ?? "0.0.0";
+        logger.info(`Starting migrations from version ${startVersion}`);
+
+        // Filter and sort migrations
+        const pendingMigrations = migrations
+            .filter((migration) => semver.gt(migration.version, startVersion))
+            .sort((a, b) => semver.compare(a.version, b.version));
+
+        // Run migrations in order
+        for (const migration of pendingMigrations) {
+            logger.info(`Running migration ${migration.version}`);
+
+            try {
+                await migration.run();
+
+                // Update version in database
+                await db
+                    .insert(versionMigrations)
+                    .values({
+                        version: migration.version,
+                        executedAt: Date.now()
+                    })
+                    .execute();
+
+                logger.info(
+                    `Successfully completed migration ${migration.version}`
+                );
+            } catch (error) {
+                logger.error(
+                    `Failed to run migration ${migration.version}:`,
+                    error
+                );
+                throw error; // Re-throw to stop migration process
+            }
+        }
 
-interface FileExecutionResult {
-    version: string;
-    success: boolean;
-    executedAt: number;
-    error?: Error;
+        logger.info("All migrations completed successfully");
+    } catch (error) {
+        logger.error("Migration process failed:", error);
+        throw error;
+    }
 }

+ 2 - 1
server/setup/scripts/1.0.0.ts

@@ -1,7 +1,8 @@
 import db from "@server/db";
 import logger from "@server/logger";
 
-export default async function run() {
+export default async function migration100() {
     logger.info("Running setup script 1.0.0");
+    // SQL operations would go here in ts format
     logger.info("Done...");
 }