From a9b0bd8b47ec9d248c2c185d191b172d71025ab8 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 28 May 2026 20:15:13 -0700 Subject: [PATCH] Alter schema + add form field --- drizzle.config.ts | 15 ++ package-lock.json | 12 ++ server/db/sqlite/schema/schema.ts | 1 + server/setup/migrations.ts | 206 +++++++++++++++++++++++++ src/components/CreateShareLinkForm.tsx | 20 ++- 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 drizzle.config.ts create mode 100644 server/setup/migrations.ts diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 000000000..d8344f942 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,15 @@ +import { APP_PATH } from "@server/lib/consts"; +import { defineConfig } from "drizzle-kit"; +import path from "path"; + +const schema = [path.join("server", "db", "sqlite", "schema")]; + +export default defineConfig({ + dialect: "sqlite", + schema: schema, + out: path.join("server", "migrations"), + verbose: true, + dbCredentials: { + url: path.join(APP_PATH, "db", "db.sqlite") + } +}); diff --git a/package-lock.json b/package-lock.json index ec51c2a5d..a706e838f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8223,12 +8223,15 @@ "license": "MIT", "optional": true, "peer": true +<<<<<<< HEAD }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", "license": "MIT" +======= +>>>>>>> db3424784 (Alter schema + add form field) }, "node_modules/@types/ws": { "version": "8.18.1", @@ -10749,9 +10752,12 @@ "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", "license": "(MPL-2.0 OR Apache-2.0)", "peer": true, +<<<<<<< HEAD "engines": { "node": ">=20" }, +======= +>>>>>>> db3424784 (Alter schema + add form field) "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -17526,9 +17532,15 @@ } }, "node_modules/tailwindcss": { +<<<<<<< HEAD "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", +======= + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", +>>>>>>> db3424784 (Alter schema + add form field) "license": "MIT" }, "node_modules/tapable": { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 2376927d2..b1947ccd7 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1101,6 +1101,7 @@ export const resourceAccessToken = sqliteTable("resourceAccessToken", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + path: text("path"), tokenHash: text("tokenHash").notNull(), sessionLength: integer("sessionLength").notNull(), expiresAt: integer("expiresAt"), diff --git a/server/setup/migrations.ts b/server/setup/migrations.ts new file mode 100644 index 000000000..0bbc86ee3 --- /dev/null +++ b/server/setup/migrations.ts @@ -0,0 +1,206 @@ +#! /usr/bin/env node +import { migrate } from "drizzle-orm/better-sqlite3/migrator"; +import { db, exists } from "../db/sqlite"; +import path from "path"; +import semver from "semver"; +import { versionMigrations } from "../db/sqlite"; +import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; +import { SqliteError } from "better-sqlite3"; +import fs from "fs"; +import m1 from "./scriptsSqlite/1.0.0-beta1"; +import m2 from "./scriptsSqlite/1.0.0-beta2"; +import m3 from "./scriptsSqlite/1.0.0-beta3"; +import m4 from "./scriptsSqlite/1.0.0-beta5"; +import m5 from "./scriptsSqlite/1.0.0-beta6"; +import m6 from "./scriptsSqlite/1.0.0-beta9"; +import m7 from "./scriptsSqlite/1.0.0-beta10"; +import m8 from "./scriptsSqlite/1.0.0-beta12"; +import m13 from "./scriptsSqlite/1.0.0-beta13"; +import m15 from "./scriptsSqlite/1.0.0-beta15"; +import m16 from "./scriptsSqlite/1.0.0"; +import m17 from "./scriptsSqlite/1.1.0"; +import m18 from "./scriptsSqlite/1.2.0"; +import m19 from "./scriptsSqlite/1.3.0"; +import m20 from "./scriptsSqlite/1.5.0"; +import m21 from "./scriptsSqlite/1.6.0"; +import m22 from "./scriptsSqlite/1.7.0"; +import m23 from "./scriptsSqlite/1.8.0"; +import m24 from "./scriptsSqlite/1.9.0"; +import m25 from "./scriptsSqlite/1.10.0"; +import m26 from "./scriptsSqlite/1.10.1"; +import m27 from "./scriptsSqlite/1.10.2"; +import m28 from "./scriptsSqlite/1.11.0"; +import m29 from "./scriptsSqlite/1.11.1"; +import m30 from "./scriptsSqlite/1.12.0"; +import m31 from "./scriptsSqlite/1.13.0"; +import m32 from "./scriptsSqlite/1.14.0"; +import m33 from "./scriptsSqlite/1.15.0"; + +// THIS CANNOT IMPORT ANYTHING FROM THE SERVER +// EXCEPT FOR THE DATABASE AND THE SCHEMA + +// Define the migration list with versions and their corresponding functions +const migrations = [ + { version: "1.0.0-beta.1", run: m1 }, + { version: "1.0.0-beta.2", run: m2 }, + { version: "1.0.0-beta.3", run: m3 }, + { version: "1.0.0-beta.5", run: m4 }, + { version: "1.0.0-beta.6", run: m5 }, + { version: "1.0.0-beta.9", run: m6 }, + { version: "1.0.0-beta.10", run: m7 }, + { version: "1.0.0-beta.12", run: m8 }, + { version: "1.0.0-beta.13", run: m13 }, + { version: "1.0.0-beta.15", run: m15 }, + { version: "1.0.0", run: m16 }, + { version: "1.1.0", run: m17 }, + { version: "1.2.0", run: m18 }, + { version: "1.3.0", run: m19 }, + { version: "1.5.0", run: m20 }, + { version: "1.6.0", run: m21 }, + { version: "1.7.0", run: m22 }, + { version: "1.8.0", run: m23 }, + { version: "1.9.0", run: m24 }, + { version: "1.10.0", run: m25 }, + { version: "1.10.1", run: m26 }, + { version: "1.10.2", run: m27 }, + { version: "1.11.0", run: m28 }, + { version: "1.11.1", run: m29 }, + { version: "1.12.0", run: m30 }, + { version: "1.13.0", run: m31 }, + { version: "1.14.0", run: m32 }, + { version: "1.15.0", run: m33 } + // Add new migrations here as they are created +] as const; + +await run(); + +async function run() { + // run the migrations + await runMigrations(); +} + +function backupDb() { + // make dir config/db/backups + const appPath = APP_PATH; + const dbDir = path.join(appPath, "db"); + + const backupsDir = path.join(dbDir, "backups"); + + // check if the backups directory exists and create it if it doesn't + if (!fs.existsSync(backupsDir)) { + fs.mkdirSync(backupsDir, { recursive: true }); + } + + // copy the db.sqlite file to backups + // add the date to the filename + const date = new Date(); + const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`; + const dbPath = path.join(dbDir, "db.sqlite"); + const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`); + fs.copyFileSync(dbPath, backupPath); +} + +export async function runMigrations() { + if (process.env.DISABLE_MIGRATIONS) { + console.log("Migrations are disabled. Skipping..."); + return; + } + try { + const appVersion = APP_VERSION; + + if (exists) { + await executeScripts(); + } else { + console.log("Running migrations..."); + try { + migrate(db, { + migrationsFolder: path.join(__DIRNAME, "init") // put here during the docker build + }); + console.log("Migrations completed successfully."); + } catch (error) { + console.error("Error running migrations:", error); + } + + await db + .insert(versionMigrations) + .values({ + version: appVersion, + executedAt: Date.now() + }) + .execute(); + } + } catch (e) { + console.error("Error running migrations:", e); + await new Promise((resolve) => + setTimeout(resolve, 1000 * 60 * 60 * 24 * 1) + ); + } +} + +async function executeScripts() { + try { + // Get the last executed version from the database + const lastExecuted = await db.select().from(versionMigrations); + + // Filter and sort migrations + const pendingMigrations = lastExecuted + .map((m) => m) + .sort((a, b) => semver.compare(b.version, a.version)); + const startVersion = pendingMigrations[0]?.version ?? APP_VERSION; + console.log(`Starting migrations from version ${startVersion}`); + + const migrationsToRun = migrations.filter((migration) => + semver.gt(migration.version, startVersion) + ); + + console.log( + "Migrations to run:", + migrationsToRun.map((m) => m.version).join(", ") + ); + + // Run migrations in order + for (const migration of migrationsToRun) { + console.log(`Running migration ${migration.version}`); + + try { + if (!process.env.DISABLE_BACKUP_ON_MIGRATION) { + // Backup the database before running the migration + backupDb(); + } + + await migration.run(); + + // Update version in database + await db + .insert(versionMigrations) + .values({ + version: migration.version, + executedAt: Date.now() + }) + .execute(); + + console.log( + `Successfully completed migration ${migration.version}` + ); + } catch (e) { + if ( + e instanceof SqliteError && + e.code === "SQLITE_CONSTRAINT_UNIQUE" + ) { + console.error("Migration has already run! Skipping..."); + continue; + } + console.error( + `Failed to run migration ${migration.version}:`, + e + ); + throw e; // Re-throw to stop migration process + } + } + + console.log("All migrations completed successfully"); + } catch (error) { + console.error("Migration process failed:", error); + throw error; + } +} diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 2e5dbe655..32b8dbd7b 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -112,6 +112,7 @@ export default function CreateShareLinkForm({ resourceId: z.number({ message: t("shareErrorSelectResource") }), resourceName: z.string(), resourceUrl: z.string(), + path: z.string().optional(), timeUnit: z.string(), timeValue: z.coerce.number().int().positive().min(1), title: z.string().optional() @@ -172,7 +173,8 @@ export default function CreateShareLinkForm({ resource: values.resourceName || "Resource" + values.resourceId - }) + }), + path: values.path } ) .catch((e) => { @@ -302,6 +304,22 @@ export default function CreateShareLinkForm({ )} /> + ( + + + {t("sharePathOptional")} + + + + + + + )} + /> +