refactor(mobility): switch to Drizzle ORM and remove raw SQLite usage

- replace Database with db instance
- use schema imports for tables
- use db.select, db.insert, onConflictDoUpdate
- remove manual connection handling and console logs
- improve type safety and maintainability

refactor(students): migrate to Drizzle ORM and async queries

Replace raw sqlite queries with Drizzle ORM. Remove the connect helper and use the
shared db instance and schema definitions. Convert getItself, getAll and
addStudents to async functions, use eq and lt helpers, and simplify promotion
handling. This improves type safety, maintainability, and allows non‑blocking
database access.
This commit is contained in:
2026-04-03 10:43:29 +02:00
parent 4949bdce5d
commit 9636242b42
2 changed files with 133 additions and 214 deletions
+49 -101
View File
@@ -1,55 +1,36 @@
import { Handlers } from "$fresh/server.ts";
import { Database } from "@db/sqlite";
import { db } from "$root/databases/db.ts";
import { mobility, promotions, students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm";
export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() {
try {
console.log("Connecting to mobility database...");
const connection = new Database("databases/data/mobility.db", {
create: false,
});
connection.run(
"ATTACH DATABASE 'databases/data/students.db' AS students",
);
console.log("Connected to databases.");
const studentRows = await db
.select({
id: students.userId,
firstName: students.firstName,
lastName: students.lastName,
promotionId: students.promotionId,
endyear: promotions.endyear,
current: promotions.current,
})
.from(students)
.leftJoin(promotions, eq(students.promotionId, promotions.id));
const students = connection.prepare(
`SELECT
students.userId AS id,
students.firstName,
students.lastName,
students.promotionId AS promotionId,
promotions.name AS promotionName
FROM students.students
LEFT JOIN students.promotions ON students.promotionId = promotions.id`,
).all();
const mobilityRows = await db.select().from(mobility);
const mobilities = connection.prepare(
`SELECT
mobility.id,
mobility.studentId,
mobility.startDate,
mobility.endDate,
mobility.weeksCount,
mobility.destinationCountry,
mobility.destinationName,
mobility.mobilityStatus
FROM mobility`,
).all();
const promotions = connection.prepare(
`SELECT id, name FROM students.promotions`,
).all();
connection.close();
const promotionRows = await db
.select({ id: promotions.id, endyear: promotions.endyear, current: promotions.current })
.from(promotions);
return new Response(
JSON.stringify({ mobilities, students, promotions }),
{
status: 200,
headers: { "Content-Type": "application/json" },
},
JSON.stringify({
mobilities: mobilityRows,
students: studentRows,
promotions: promotionRows,
}),
{ status: 200, headers: { "Content-Type": "application/json" } },
);
} catch (error) {
console.error("Error fetching mobility data:", error);
@@ -58,8 +39,6 @@ export const handler: Handlers = {
},
async POST(request) {
console.log("API /mobility/api/insert_mobility POST called");
try {
const body = await request.json();
const { data } = body;
@@ -67,32 +46,8 @@ export const handler: Handlers = {
if (!Array.isArray(data)) {
throw new Error("Invalid request body");
}
console.log("Connecting to mobility database...");
const connection = new Database("databases/data/mobility.db", {
create: false,
});
console.log("Attaching students database...");
connection.run(
"ATTACH DATABASE 'databases/data/students.db' AS students",
);
console.log("Students database attached successfully.");
const insertQuery = connection.prepare(
`INSERT INTO mobility (
id, studentId, startDate, endDate, weeksCount, destinationCountry, destinationName, mobilityStatus
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
startDate = excluded.startDate,
endDate = excluded.endDate,
weeksCount = excluded.weeksCount,
destinationCountry = excluded.destinationCountry,
destinationName = excluded.destinationName,
mobilityStatus = excluded.mobilityStatus`,
);
for (const mobility of data) {
for (const entry of data) {
const {
id,
studentId,
@@ -102,19 +57,16 @@ export const handler: Handlers = {
destinationCountry,
destinationName,
mobilityStatus = "N/A",
} = mobility;
} = entry;
console.log("Processing mobility data:", mobility);
const studentExists = await db
.select({ userId: students.userId })
.from(students)
.where(eq(students.userId, studentId))
.limit(1)
.then((rows) => rows.length > 0);
const studentExists = connection
.prepare(
`SELECT COUNT(*) AS count FROM students.students WHERE userId = ?`,
)
.get(studentId);
console.log(`Student ${studentId} exists:`, studentExists.count > 0);
if (studentExists.count === 0) {
if (!studentExists) {
console.warn(`Skipping mobility for unknown studentId: ${studentId}`);
continue;
}
@@ -123,43 +75,39 @@ export const handler: Handlers = {
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
if (start <= end) {
calculatedWeeksCount = Math.ceil(
calculatedWeeksCount = start <= end
? Math.ceil(
(end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000),
);
} else {
calculatedWeeksCount = null;
}
)
: null;
}
console.log("Executing SQL insert/update query for:", {
await db
.insert(mobility)
.values({
id,
studentId,
startDate,
endDate,
calculatedWeeksCount,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
});
insertQuery.run(
id,
studentId,
})
.onConflictDoUpdate({
target: mobility.id,
set: {
startDate,
endDate,
calculatedWeeksCount,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
);
},
});
}
connection.close();
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", {
status: 200,
});
return new Response("Data inserted/updated successfully", { status: 200 });
} catch (error) {
console.error("Error inserting mobility data:", error);
return new Response("Failed to insert/update data", { status: 500 });
+72 -101
View File
@@ -1,150 +1,121 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
import { db } from "$root/databases/db.ts";
import { promotions, students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { Database } from "@db/sqlite";
import { eq, lt } from "npm:drizzle-orm";
/**
* Gets itself from the database.
* @param database The database connection
* @param userId The user ID.
* @returns Itself from the database.
*/
function getItself(
database: Database,
async function getItself(
userId: string,
): { student: Student | null; promo: Promotion | null } {
const studentQuery = "select * from students where userId = ?";
const student: Student | undefined = database.prepare(studentQuery).get(
userId,
);
): Promise<{ student: Student | null; promo: Promotion | null }> {
const student = await db
.select()
.from(students)
.where(eq(students.userId, userId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!student) {
return { student: null, promo: null };
}
const promoQuery = "select * from promotions where id = ?";
const promo: Promotion | undefined = database.prepare(promoQuery).get(
student.promotionId,
);
const promo = await db
.select()
.from(promotions)
.where(eq(promotions.id, student.promotionId!))
.limit(1)
.then((rows) => rows[0] ?? null);
return { student, promo: promo ?? null };
return { student, promo };
}
/**
* Gets itself from the database.
* @param database The database connexion
* @param userId The user ID.
* @returns Itself from the database.
*/
function getAll(
database: Database,
): { students: Student[]; promos: Promotion[] } {
const studentsQuery = `
select userId, firstName, lastName, mail, promotionId
from students inner join promotions
on students.promotionId = promotions.id
where promotions.current < 6`;
const students: Student[] = database.prepare(studentsQuery).all();
async function getAll(): Promise<
{ students: Student[]; promos: Promotion[] }
> {
const rows = await db
.select({
userId: students.userId,
firstName: students.firstName,
lastName: students.lastName,
mail: students.mail,
promotionId: students.promotionId,
})
.from(students)
.innerJoin(promotions, eq(students.promotionId, promotions.id))
.where(lt(promotions.current, 6));
const promosQuery = "select * from promotions where promotions.current < 6";
const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
const promos = await db
.select()
.from(promotions)
.where(lt(promotions.current, 6));
return { students, promos };
return { students: rows as Student[], promos };
}
/**
* Add users to the database.
* @param database The database connexion
* @param students The students to add
* @param promoId The promotion id.
*/
function addStudents(database: Database, students: Student[], promoId: string) {
const query = `
INSERT INTO students
(userId, firstName, lastName, mail, promotionId)
VALUES (?, ?, ?, ?, ?)`;
const statement = database.prepare(query);
for (const student of students) {
statement.run(
student.userId,
student.firstName,
student.lastName,
student.mail,
promoId,
);
async function addStudents(
studentList: Student[],
promoId: number,
): Promise<void> {
for (const student of studentList) {
await db
.insert(students)
.values({
userId: student.userId,
firstName: student.firstName,
lastName: student.lastName,
mail: student.mail,
promotionId: promoId,
})
.onConflictDoNothing();
}
}
export const handler: Handlers<null, AuthenticatedState> = {
/**
* The students the user can see.
* @param _request The HTTP request.
* @param _context The context with authenticated state.
* @returns All students our user can see.
*/
// deno-lint-ignore require-await
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
using connection = connect("students");
const database = connection.database;
if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response(
JSON.stringify(getItself(database, context.state.session.uid)),
{
headers: {
"content-type": "application/json",
},
},
JSON.stringify(await getItself(context.state.session.uid)),
{ headers: { "content-type": "application/json" } },
);
}
return new Response(
JSON.stringify(getAll(database)),
{
headers: {
"content-type": "application/json",
},
},
JSON.stringify(await getAll()),
{ headers: { "content-type": "application/json" } },
);
},
/**
* Add students in the database.
* @param request The HTTP request.
* @param _context The Fresh context.
* @returns HTTP 201 on successful insert.
*/
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const { students, promo }: { students: Student[]; promo: string } =
await request.json();
const { students: studentList, promo }: {
students: Student[];
promo: string;
} = await request.json();
if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(studentList)) {
return new Response(null, { status: 400 });
}
using connection = connect("students");
const database = connection.database;
const { endyear, current } = promo.match(
/^(?<endyear>\d{4})-(?<current>\d)A$/,
)?.groups!;
database.prepare(
"insert or ignore into promotions (endyear, current) values (?, ?)",
).run(endyear, current);
await db
.insert(promotions)
.values({ endyear: Number(endyear), current: Number(current) })
.onConflictDoNothing();
const { id: promoId }: { id: string } = database
.prepare("select id from promotions where endyear = ? and current = ?")
.get(endyear, current)!;
const promo_row = await db
.select()
.from(promotions)
.where(eq(promotions.endyear, Number(endyear)))
.then((rows) => rows.find((r) => r.current === Number(current))!);
addStudents(database, students, promoId);
await addStudents(studentList, promo_row.id);
return new Response(null, { status: 201 });
},