mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-10 14:04:51 +02:00
Complete migration setup
This commit is contained in:
parent
907504eefb
commit
993eab5ac1
5 changed files with 79 additions and 129 deletions
|
@ -24,7 +24,7 @@ RUN npm install --omit=dev --legacy-peer-deps
|
||||||
|
|
||||||
COPY --from=builder /app/.next ./.next
|
COPY --from=builder /app/.next ./.next
|
||||||
COPY --from=builder /app/dist ./dist
|
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 config.example.yml ./dist/config.example.yml
|
||||||
COPY server/db/names.json ./dist/names.json
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
|
|
@ -3,10 +3,21 @@ import Database from "better-sqlite3";
|
||||||
import * as schema from "@server/db/schema";
|
import * as schema from "@server/db/schema";
|
||||||
import { APP_PATH } from "@server/config";
|
import { APP_PATH } from "@server/config";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
|
||||||
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
export const location = path.join(APP_PATH, "db", "db.sqlite");
|
||||||
|
export const exists = await checkFileExists(location);
|
||||||
|
|
||||||
const sqlite = new Database(location);
|
const sqlite = new Database(location);
|
||||||
export const db = drizzle(sqlite, { schema });
|
export const db = drizzle(sqlite, { schema });
|
||||||
|
|
||||||
export default db;
|
export default db;
|
||||||
|
|
||||||
|
async function checkFileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -394,3 +394,5 @@ export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
|
export type VersionMigration = InferSelectModel<typeof versionMigrations>;
|
|
@ -1,12 +1,21 @@
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { __DIRNAME } from "@server/config";
|
import { __DIRNAME } from "@server/config";
|
||||||
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
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 path from "path";
|
||||||
import * as fs from "fs/promises";
|
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { versionMigrations } from "@server/db/schema";
|
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() {
|
export async function runMigrations() {
|
||||||
if (!process.env.APP_VERSION) {
|
if (!process.env.APP_VERSION) {
|
||||||
|
@ -18,122 +27,13 @@ export async function runMigrations() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await checkFileExists(location)) {
|
if (exists) {
|
||||||
try {
|
await executeScripts();
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
logger.info("Running migrations...");
|
logger.info("Running migrations...");
|
||||||
try {
|
try {
|
||||||
migrate(db, {
|
migrate(db, {
|
||||||
migrationsFolder: path.join(__DIRNAME, "init")
|
migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build
|
||||||
});
|
});
|
||||||
logger.info("Migrations completed successfully.");
|
logger.info("Migrations completed successfully.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -151,18 +51,54 @@ export async function runMigrations() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkFileExists(filePath: string): Promise<boolean> {
|
async function executeScripts() {
|
||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
// Get the last executed version from the database
|
||||||
return true;
|
const lastExecuted = await db
|
||||||
} catch {
|
.select()
|
||||||
return false;
|
.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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import db from "@server/db";
|
import db from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
|
|
||||||
export default async function run() {
|
export default async function migration100() {
|
||||||
logger.info("Running setup script 1.0.0");
|
logger.info("Running setup script 1.0.0");
|
||||||
|
// SQL operations would go here in ts format
|
||||||
logger.info("Done...");
|
logger.info("Done...");
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue