Complete migration setup

This commit is contained in:
Owen Schwartz 2024-12-25 16:40:39 -05:00
parent 907504eefb
commit 993eab5ac1
No known key found for this signature in database
GPG key ID: 8271FDFFD9E0CCBD
5 changed files with 79 additions and 129 deletions

View file

@ -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

View file

@ -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;
}
}

View file

@ -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>;

View file

@ -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;
}
}

View file

@ -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...");
}