diff --git a/Dockerfile b/Dockerfile index fec07003..a5be0f1a 100644 --- a/Dockerfile +++ b/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 diff --git a/server/db/index.ts b/server/db/index.ts index b404c0a1..ce164f97 100644 --- a/server/db/index.ts +++ b/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 { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} \ No newline at end of file diff --git a/server/db/schema.ts b/server/db/schema.ts index 190ad7f3..4b4145d6 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -394,3 +394,5 @@ export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; +export type ResourceWhitelist = InferSelectModel; +export type VersionMigration = InferSelectModel; \ No newline at end of file diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts index 9aa8c941..3904b162 100644 --- a/server/setup/migrations.ts +++ b/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 { +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 + } + } + + logger.info("All migrations completed successfully"); + } catch (error) { + logger.error("Migration process failed:", error); + throw error; } } - -interface FileExecutionResult { - version: string; - success: boolean; - executedAt: number; - error?: Error; -} diff --git a/server/setup/scripts/1.0.0.ts b/server/setup/scripts/1.0.0.ts index 0c600251..a784e742 100644 --- a/server/setup/scripts/1.0.0.ts +++ b/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..."); } \ No newline at end of file