start migrationg to prisma-orm

This commit is contained in:
Rdeisenroth 2025-11-06 22:53:08 +01:00
parent 9a8d7a36b9
commit ae2cd5e61f
No known key found for this signature in database
GPG key ID: 197008FA42DE7127
18 changed files with 374 additions and 200 deletions

View file

@ -1,5 +1,5 @@
import { Cookie } from "elysia";
import db from "../db/db";
import prisma from "../db/db";
import { MAX_CONVERT_PROCESS } from "../helpers/env";
import { normalizeFiletype, normalizeOutputFiletype } from "../helpers/normalizeFiletype";
import { convert as convertassimp, properties as propertiesassimp } from "./assimp";
@ -146,10 +146,6 @@ export async function handleConvert(
converterName: string,
jobId: Cookie<string | undefined>,
) {
const query = db.query(
"INSERT INTO file_names (job_id, file_name, output_file_name, status) VALUES (?1, ?2, ?3, ?4)",
);
for (const chunk of chunks(fileNames, MAX_CONVERT_PROCESS)) {
const toProcess: Promise<string>[] = [];
for (const fileName of chunk) {
@ -165,9 +161,16 @@ export async function handleConvert(
toProcess.push(
new Promise((resolve, reject) => {
mainConverter(filePath, fileType, convertTo, targetPath, {}, converterName)
.then((r) => {
.then(async (r) => {
if (jobId.value) {
query.run(jobId.value, fileName, newFileName, r);
await prisma.fileName.create({
data: {
jobId: parseInt(jobId.value, 10),
fileName,
outputFileName: newFileName,
status: r,
},
});
}
resolve(r);
})

View file

@ -1,41 +1,8 @@
import { Database } from "bun:sqlite";
import { PrismaClient } from "@prisma/client";
const db = new Database("./data/mydb.sqlite", { create: true });
const prisma = new PrismaClient();
if (!db.query("SELECT * FROM sqlite_master WHERE type='table'").get()) {
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL,
password TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_names (
id INTEGER PRIMARY KEY AUTOINCREMENT,
job_id INTEGER NOT NULL,
file_name TEXT NOT NULL,
output_file_name TEXT NOT NULL,
status TEXT DEFAULT 'not started',
FOREIGN KEY (job_id) REFERENCES jobs(id)
);
CREATE TABLE IF NOT EXISTS jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
date_created TEXT NOT NULL,
status TEXT DEFAULT 'not started',
num_files INTEGER DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id)
);
PRAGMA user_version = 1;`);
}
// Initialize the database schema using Prisma migrations instead of raw SQL.
// Ensure to run `npx prisma migrate dev` to apply the schema changes.
const dbVersion = (db.query("PRAGMA user_version").get() as { user_version?: number }).user_version;
if (dbVersion === 0) {
db.exec("ALTER TABLE file_names ADD COLUMN status TEXT DEFAULT 'not started';");
db.exec("PRAGMA user_version = 1;");
console.log("Updated database to version 1.");
}
// enable WAL mode
db.exec("PRAGMA journal_mode = WAL;");
export default db;
export default prisma;

View file

@ -1,23 +0,0 @@
export class Filename {
id!: number;
job_id!: number;
file_name!: string;
output_file_name!: string;
status!: string;
}
export class Jobs {
finished_files!: number;
id!: number;
user_id!: number;
date_created!: string;
status!: string;
num_files!: number;
files_detailed!: Filename[];
}
export class User {
id!: number;
email!: string;
password!: string;
}

View file

@ -4,8 +4,7 @@ import { html } from "@elysiajs/html";
import { staticPlugin } from "@elysiajs/static";
import { Elysia } from "elysia";
import "./helpers/printVersions";
import db from "./db/db";
import { Jobs } from "./db/types";
import prisma from "./db/db";
import { AUTO_DELETE_EVERY_N_HOURS, WEBROOT } from "./helpers/env";
import { chooseConverter } from "./pages/chooseConverter";
import { convert } from "./pages/convert";
@ -69,25 +68,28 @@ app.listen(3000);
console.log(`🦊 Elysia is running at http://${app.server?.hostname}:${app.server?.port}${WEBROOT}`);
const clearJobs = () => {
const jobs = db
.query("SELECT * FROM jobs WHERE date_created < ?")
.as(Jobs)
.all(new Date(Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000).toISOString());
const clearJobs = async () => {
const jobs = await prisma.job.findMany({
where: {
dateCreated: {
lt: new Date(Date.now() - AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000),
},
},
});
for (const job of jobs) {
// delete the directories
rmSync(`${outputDir}${job.user_id}/${job.id}`, {
rmSync(`${outputDir}${job.userId}/${job.id}`, {
recursive: true,
force: true,
});
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
rmSync(`${uploadsDir}${job.userId}/${job.id}`, {
recursive: true,
force: true,
});
// delete the job
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
await prisma.job.delete({ where: { id: job.id } });
}
setTimeout(clearJobs, AUTO_DELETE_EVERY_N_HOURS * 60 * 60 * 1000);

View file

@ -3,8 +3,7 @@ import { Elysia, t } from "elysia";
import sanitize from "sanitize-filename";
import { outputDir, uploadsDir } from "..";
import { handleConvert } from "../converters/main";
import db from "../db/db";
import { Jobs } from "../db/types";
import prisma from "../db/db";
import { WEBROOT } from "../helpers/env";
import { normalizeFiletype } from "../helpers/normalizeFiletype";
import { userService } from "./user";
@ -25,10 +24,15 @@ export const convert = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.as(Jobs)
.get(jobId.value, user.id);
const parsedJobId = parseInt(jobId.value, 10);
const parsedUserId = parseInt(user.id, 10);
const existingJob = await prisma.job.findFirst({
where: {
id: parsedJobId,
userId: parsedUserId,
},
});
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);
@ -61,17 +65,23 @@ export const convert = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}
db.query("UPDATE jobs SET num_files = ?1, status = 'pending' WHERE id = ?2").run(
fileNames.length,
jobId.value,
);
await prisma.job.update({
where: { id: parsedJobId },
data: {
numFiles: fileNames.length,
status: "pending",
},
});
// Start the conversion process in the background
handleConvert(fileNames, userUploadsDir, userOutputDir, convertTo, converterName, jobId)
.then(() => {
.then(async () => {
// All conversions are done, update the job status to 'completed'
if (jobId.value) {
db.query("UPDATE jobs SET status = 'completed' WHERE id = ?1").run(jobId.value);
await prisma.job.update({
where: { id: parsedJobId },
data: { status: "completed" },
});
}
// Delete all uploaded files in userUploadsDir

View file

@ -12,9 +12,12 @@ export const deleteFile = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
const existingJob = await db.job.findFirst({
where: {
id: parseInt(jobId.value, 10),
userId: parseInt(user.id, 10),
},
});
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);

View file

@ -4,32 +4,35 @@ import { outputDir, uploadsDir } from "..";
import db from "../db/db";
import { WEBROOT } from "../helpers/env";
import { userService } from "./user";
import { Jobs } from "../db/types";
export const deleteJob = new Elysia().use(userService).get(
"/delete/:userId/:jobId",
async ({ params, redirect, user }) => {
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
const job = await db.job.findFirst({
where: {
userId: parseInt(user.id, 10),
id: parseInt(params.jobId, 10),
},
});
if (!job) {
return redirect(`${WEBROOT}/results`, 302);
}
// delete the directories
rmSync(`${outputDir}${job.user_id}/${job.id}`, {
rmSync(`${outputDir}${job.userId}/${job.id}`, {
recursive: true,
force: true,
});
rmSync(`${uploadsDir}${job.user_id}/${job.id}`, {
rmSync(`${uploadsDir}${job.userId}/${job.id}`, {
recursive: true,
force: true,
});
// delete the job
db.query("DELETE FROM jobs WHERE id = ?").run(job.id);
await db.job.delete({
where: { id: job.id },
});
return redirect(`${WEBROOT}/history`, 302);
},
{

View file

@ -12,9 +12,12 @@ export const download = new Elysia()
.get(
"/download/:userId/:jobId/:fileName",
async ({ params, redirect, user }) => {
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
const job = await db.job.findFirst({
where: {
userId: parseInt(user.id, 10),
id: parseInt(params.jobId, 10),
},
});
if (!job) {
return redirect(`${WEBROOT}/results`, 302);
@ -34,9 +37,12 @@ export const download = new Elysia()
.get(
"/archive/:userId/:jobId",
async ({ params, redirect, user }) => {
const job = await db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.get(user.id, params.jobId);
const job = await db.job.findFirst({
where: {
userId: parseInt(user.id, 10),
id: parseInt(params.jobId, 10),
},
});
if (!job) {
return redirect(`${WEBROOT}/results`, 302);

View file

@ -1,8 +1,7 @@
import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import prisma from "../db/db";
import { ALLOW_UNAUTHENTICATED, HIDE_HISTORY, LANGUAGE, WEBROOT } from "../helpers/env";
import { userService } from "./user";
@ -17,17 +16,20 @@ export const history = new Elysia().use(userService).get(
return redirect(`${WEBROOT}/login`, 302);
}
let userJobs = db.query("SELECT * FROM jobs WHERE user_id = ?").as(Jobs).all(user.id).reverse();
const userId = parseInt(user.id, 10);
for (const job of userJobs) {
const files = db.query("SELECT * FROM file_names WHERE job_id = ?").as(Filename).all(job.id);
job.finished_files = files.length;
job.files_detailed = files;
}
// Filter out jobs with no files
userJobs = userJobs.filter((job) => job.num_files > 0);
const userJobs = await prisma.job.findMany({
where: {
userId,
files: {
some: {}, // Ensures only jobs with files are included
},
},
orderBy: { id: "desc" },
include: {
files: true,
},
});
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Results">
@ -127,9 +129,9 @@ export const history = new Elysia().use(userService).get(
/>
</svg>
</td>
<td safe>{new Date(job.date_created).toLocaleTimeString(LANGUAGE)}</td>
<td>{job.num_files}</td>
<td class="max-sm:hidden">{job.finished_files}</td>
<td safe>{new Date(job.dateCreated).toLocaleTimeString(LANGUAGE)}</td>
<td>{job.numFiles}</td>
<td class="max-sm:hidden">{job.files.length}</td>
<td safe>{job.status}</td>
<td>
<a
@ -147,10 +149,10 @@ export const history = new Elysia().use(userService).get(
<td colspan="6">
<div class="p-2 text-sm text-neutral-500">
<div class="mb-1 font-semibold">Detailed File Information:</div>
{job.files_detailed.map((file: Filename) => (
{job.files.map((file) => (
<div class="flex items-center">
<span class="w-5/12 truncate" title={file.file_name} safe>
{file.file_name}
<span class="w-5/12 truncate" title={file.fileName} safe>
{file.fileName}
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -164,8 +166,8 @@ export const history = new Elysia().use(userService).get(
clip-rule="evenodd"
/>
</svg>
<span class="w-5/12 truncate" title={file.output_file_name} safe>
{file.output_file_name}
<span class="w-5/12 truncate" title={file.outputFileName} safe>
{file.outputFileName}
</span>
</div>
))}

View file

@ -3,7 +3,7 @@ import { Elysia } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { Filename, Jobs } from "../db/types";
import { Job, FileName } from "@prisma/client";
import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env";
import { DownloadIcon } from "../icons/download";
import { DeleteIcon } from "../icons/delete";
@ -19,8 +19,8 @@ function ResultsArticle({
user: {
id: string;
} & JWTPayloadSpec;
job: Jobs;
files: Filename[];
job: Job;
files: FileName[];
outputPath: string;
}) {
return (
@ -29,11 +29,11 @@ function ResultsArticle({
<h1 class="text-xl">Results</h1>
<div class="flex flex-row gap-4">
<a
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
style={files.length !== job.numFiles ? "pointer-events: none;" : ""}
href={`${WEBROOT}/archive/${user.id}/${job.id}`}
download={`converted_files_${job.id}.tar`}
class="flex btn-primary flex-row gap-2 text-contrast"
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
{...(files.length !== job.numFiles ? { disabled: true, "aria-busy": "true" } : "")}
>
<DownloadIcon /> <p>Tar</p>
</a>
@ -41,18 +41,18 @@ function ResultsArticle({
<DownloadIcon /> <p>All</p>
</button>
<a
style={files.length !== job.num_files ? "pointer-events: none;" : ""}
style={files.length !== job.numFiles ? "pointer-events: none;" : ""}
class="flex btn-primary flex-row gap-2 text-contrast"
href={`${WEBROOT}/delete/${user.id}/${job.id}`}
{...(files.length !== job.num_files ? { disabled: true, "aria-busy": "true" } : "")}
{...(files.length !== job.numFiles ? { disabled: true, "aria-busy": "true" } : "")}
>
<DeleteIcon /> <p>Delete</p>
</a>
</div>
</div>
<progress
max={job.num_files}
{...(files.length === job.num_files ? { value: files.length } : "")}
max={job.numFiles}
{...(files.length === job.numFiles ? { value: files.length } : "")}
class={`
mb-4 inline-block h-2 w-full appearance-none overflow-hidden rounded-full border-0
bg-neutral-700 bg-none text-accent-500 accent-accent-500
@ -101,7 +101,7 @@ function ResultsArticle({
{files.map((file) => (
<tr>
<td safe class="max-w-[20vw] truncate">
{file.output_file_name}
{file.outputFileName}
</td>
<td safe>{file.status}</td>
<td class="flex flex-row gap-4">
@ -110,7 +110,7 @@ function ResultsArticle({
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
href={`${WEBROOT}/download/${outputPath}${file.outputFileName}`}
>
<EyeIcon />
</a>
@ -119,8 +119,8 @@ function ResultsArticle({
text-accent-500 underline
hover:text-accent-400
`}
href={`${WEBROOT}/download/${outputPath}${file.output_file_name}`}
download={file.output_file_name}
href={`${WEBROOT}/download/${outputPath}${file.outputFileName}`}
download={file.outputFileName}
>
<DownloadIcon />
</a>
@ -143,10 +143,15 @@ export const results = new Elysia()
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
const userId = parseInt(user.id, 10);
const jobId = parseInt(params.jobId, 10);
const job = await db.job.findFirst({
where: {
userId,
id: jobId,
},
});
if (!job) {
set.status = 404;
@ -155,12 +160,13 @@ export const results = new Elysia()
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const outputPath = `${userId}/${jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
const files = await db.fileName.findMany({
where: {
jobId,
},
});
return (
<BaseHtml webroot={WEBROOT} title="ConvertX | Result">
@ -189,10 +195,15 @@ export const results = new Elysia()
job_id.remove();
}
const job = db
.query("SELECT * FROM jobs WHERE user_id = ? AND id = ?")
.as(Jobs)
.get(user.id, params.jobId);
const userId = parseInt(user.id, 10);
const jobId = parseInt(params.jobId, 10);
const job = await db.job.findFirst({
where: {
userId,
id: jobId,
},
});
if (!job) {
set.status = 404;
@ -201,12 +212,13 @@ export const results = new Elysia()
};
}
const outputPath = `${user.id}/${params.jobId}/`;
const outputPath = `${userId}/${jobId}/`;
const files = db
.query("SELECT * FROM file_names WHERE job_id = ?")
.as(Filename)
.all(params.jobId);
const files = await db.fileName.findMany({
where: {
jobId,
},
});
return <ResultsArticle user={user} job={job} files={files} outputPath={outputPath} />;
},

View file

@ -5,7 +5,6 @@ import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import { getAllTargets } from "../converters/main";
import db from "../db/db";
import { User } from "../db/types";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
@ -31,6 +30,8 @@ export const root = new Elysia().use(userService).get(
// validate jwt
let user: ({ id: string } & JWTPayloadSpec) | false = false;
let userId: number | null = null;
if (ALLOW_UNAUTHENTICATED) {
const newUserId = String(
UNAUTHENTICATED_USER_SHARING
@ -42,6 +43,8 @@ export const root = new Elysia().use(userService).get(
});
user = { id: newUserId };
userId = parseInt(newUserId, 10);
if (!auth) {
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
@ -64,8 +67,13 @@ export const root = new Elysia().use(userService).get(
user.id &&
(Number.parseInt(user.id) < 2 ** 24 || !ALLOW_UNAUTHENTICATED)
) {
// Make sure user exists in db
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
userId = parseInt(user.id, 10);
const existingUser = await db.user.findFirst({
where: {
id: userId,
},
});
if (!existingUser) {
if (auth?.value) {
@ -76,19 +84,29 @@ export const root = new Elysia().use(userService).get(
}
}
if (!user) {
if (!user || userId === null) {
return redirect(`${WEBROOT}/login`, 302);
}
// create a new job
db.query("INSERT INTO jobs (user_id, date_created) VALUES (?, ?)").run(
user.id,
new Date().toISOString(),
);
await db.job.create({
data: {
userId,
dateCreated: new Date().toISOString(),
},
});
const { id } = db
.query("SELECT id FROM jobs WHERE user_id = ? ORDER BY id DESC")
.get(user.id) as { id: number };
const { id } = await db.job.findFirst({
where: {
userId,
},
orderBy: {
id: "desc",
},
select: {
id: true,
},
}) as { id: number };
if (!jobId) {
return { message: "Cookies should be enabled to use this app." };

View file

@ -11,9 +11,12 @@ export const upload = new Elysia().use(userService).post(
return redirect(`${WEBROOT}/`, 302);
}
const existingJob = await db
.query("SELECT * FROM jobs WHERE id = ? AND user_id = ?")
.get(jobId.value, user.id);
const existingJob = await db.job.findFirst({
where: {
id: parseInt(jobId.value, 10),
userId: parseInt(user.id, 10),
},
});
if (!existingJob) {
return redirect(`${WEBROOT}/`, 302);

View file

@ -1,10 +1,7 @@
import { randomUUID } from "node:crypto";
import { jwt } from "@elysiajs/jwt";
import { Elysia, t } from "elysia";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
import db from "../db/db";
import { User } from "../db/types";
import prisma from "../db/db";
import {
ACCOUNT_REGISTRATION,
ALLOW_UNAUTHENTICATED,
@ -12,8 +9,10 @@ import {
HTTP_ALLOWED,
WEBROOT,
} from "../helpers/env";
import { BaseHtml } from "../components/base";
import { Header } from "../components/header";
export let FIRST_RUN = db.query("SELECT * FROM users").get() === null || false;
export let FIRST_RUN = (await prisma.user.findFirst()) === null || false;
export const userService = new Elysia({ name: "user/service" })
.use(
@ -191,7 +190,7 @@ export const user = new Elysia()
FIRST_RUN = false;
}
const existingUser = await db.query("SELECT * FROM users WHERE email = ?").get(email);
const existingUser = await prisma.user.findUnique({ where: { email } });
if (existingUser) {
set.status = 400;
return {
@ -200,9 +199,12 @@ export const user = new Elysia()
}
const savedPassword = await Bun.password.hash(password);
db.query("INSERT INTO users (email, password) VALUES (?, ?)").run(email, savedPassword);
const user = db.query("SELECT * FROM users WHERE email = ?").as(User).get(email);
const user = await prisma.user.create({
data: {
email,
password: savedPassword,
},
});
if (!user) {
set.status = 500;
@ -218,7 +220,7 @@ export const user = new Elysia()
if (!auth) {
set.status = 500;
return {
message: "No auth cookie, perhaps your browser is blocking cookies.",
message: "Failed to authenticate user.",
};
}
@ -318,7 +320,7 @@ export const user = new Elysia()
.post(
"/login",
async function handler({ body, set, redirect, jwt, cookie: { auth } }) {
const existingUser = db.query("SELECT * FROM users WHERE email = ?").as(User).get(body.email);
const existingUser = await prisma.user.findUnique({ where: { email: body.email } });
if (!existingUser) {
set.status = 403;
@ -381,7 +383,9 @@ export const user = new Elysia()
return redirect(`${WEBROOT}/`, 302);
}
const userData = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
const userId = Number(user.id);
const userData = await prisma.user.findUnique({ where: { id: userId } });
if (!userData) {
return redirect(`${WEBROOT}/`, 302);
@ -465,7 +469,10 @@ export const user = new Elysia()
if (!user) {
return redirect(`${WEBROOT}/login`, 302);
}
const existingUser = db.query("SELECT * FROM users WHERE id = ?").as(User).get(user.id);
const userId = Number(user.id);
const existingUser = await prisma.user.findUnique({ where: { id: userId } });
if (!existingUser) {
if (auth?.value) {
@ -483,30 +490,22 @@ export const user = new Elysia()
};
}
const fields = [];
const values = [];
const updates: { email?: string; password?: string } = {};
if (body.email) {
const existingUser = await db
.query("SELECT id FROM users WHERE email = ?")
.as(User)
.get(body.email);
if (existingUser && existingUser.id.toString() !== user.id) {
const emailInUse = await prisma.user.findUnique({ where: { email: body.email } });
if (emailInUse && emailInUse.id !== userId) {
set.status = 409;
return { message: "Email already in use." };
}
fields.push("email");
values.push(body.email);
updates.email = body.email;
}
if (body.newPassword) {
fields.push("password");
values.push(await Bun.password.hash(body.newPassword));
updates.password = await Bun.password.hash(body.newPassword);
}
if (fields.length > 0) {
db.query(
`UPDATE users SET ${fields.map((field) => `${field}=?`).join(", ")} WHERE id=?`,
).run(...values, user.id);
if (Object.keys(updates).length > 0) {
await prisma.user.update({ where: { id: userId }, data: updates });
}
return redirect(`${WEBROOT}/`, 302);