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
+61 -113
View File
@@ -1,55 +1,36 @@
import { Handlers } from "$fresh/server.ts"; 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 = { export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() { async GET() {
try { try {
console.log("Connecting to mobility database..."); const studentRows = await db
const connection = new Database("databases/data/mobility.db", { .select({
create: false, id: students.userId,
}); firstName: students.firstName,
connection.run( lastName: students.lastName,
"ATTACH DATABASE 'databases/data/students.db' AS students", promotionId: students.promotionId,
); endyear: promotions.endyear,
console.log("Connected to databases."); current: promotions.current,
})
.from(students)
.leftJoin(promotions, eq(students.promotionId, promotions.id));
const students = connection.prepare( const mobilityRows = await db.select().from(mobility);
`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 mobilities = connection.prepare( const promotionRows = await db
`SELECT .select({ id: promotions.id, endyear: promotions.endyear, current: promotions.current })
mobility.id, .from(promotions);
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();
return new Response( return new Response(
JSON.stringify({ mobilities, students, promotions }), JSON.stringify({
{ mobilities: mobilityRows,
status: 200, students: studentRows,
headers: { "Content-Type": "application/json" }, promotions: promotionRows,
}, }),
{ status: 200, headers: { "Content-Type": "application/json" } },
); );
} catch (error) { } catch (error) {
console.error("Error fetching mobility data:", error); console.error("Error fetching mobility data:", error);
@@ -58,8 +39,6 @@ export const handler: Handlers = {
}, },
async POST(request) { async POST(request) {
console.log("API /mobility/api/insert_mobility POST called");
try { try {
const body = await request.json(); const body = await request.json();
const { data } = body; const { data } = body;
@@ -67,32 +46,8 @@ export const handler: Handlers = {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error("Invalid request body"); 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..."); for (const entry of data) {
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) {
const { const {
id, id,
studentId, studentId,
@@ -102,19 +57,16 @@ export const handler: Handlers = {
destinationCountry, destinationCountry,
destinationName, destinationName,
mobilityStatus = "N/A", 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 if (!studentExists) {
.prepare(
`SELECT COUNT(*) AS count FROM students.students WHERE userId = ?`,
)
.get(studentId);
console.log(`Student ${studentId} exists:`, studentExists.count > 0);
if (studentExists.count === 0) {
console.warn(`Skipping mobility for unknown studentId: ${studentId}`); console.warn(`Skipping mobility for unknown studentId: ${studentId}`);
continue; continue;
} }
@@ -123,43 +75,39 @@ export const handler: Handlers = {
if (startDate && endDate) { if (startDate && endDate) {
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
if (start <= end) { calculatedWeeksCount = start <= end
calculatedWeeksCount = Math.ceil( ? Math.ceil(
(end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000), (end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000),
); )
} else { : null;
calculatedWeeksCount = null;
}
} }
console.log("Executing SQL insert/update query for:", { await db
id, .insert(mobility)
studentId, .values({
startDate, id,
endDate, studentId,
calculatedWeeksCount, startDate,
destinationCountry, endDate,
destinationName, weeksCount: calculatedWeeksCount,
mobilityStatus, destinationCountry,
}); destinationName,
mobilityStatus,
insertQuery.run( })
id, .onConflictDoUpdate({
studentId, target: mobility.id,
startDate, set: {
endDate, startDate,
calculatedWeeksCount, endDate,
destinationCountry, weeksCount: calculatedWeeksCount,
destinationName, destinationCountry,
mobilityStatus, destinationName,
); mobilityStatus,
},
});
} }
connection.close(); return new Response("Data inserted/updated successfully", { status: 200 });
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", {
status: 200,
});
} catch (error) { } catch (error) {
console.error("Error inserting mobility data:", error); console.error("Error inserting mobility data:", error);
return new Response("Failed to insert/update data", { status: 500 }); 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 { 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 { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { Database } from "@db/sqlite"; import { eq, lt } from "npm:drizzle-orm";
/** async function getItself(
* Gets itself from the database.
* @param database The database connection
* @param userId The user ID.
* @returns Itself from the database.
*/
function getItself(
database: Database,
userId: string, userId: string,
): { student: Student | null; promo: Promotion | null } { ): Promise<{ student: Student | null; promo: Promotion | null }> {
const studentQuery = "select * from students where userId = ?"; const student = await db
const student: Student | undefined = database.prepare(studentQuery).get( .select()
userId, .from(students)
); .where(eq(students.userId, userId))
.limit(1)
.then((rows) => rows[0] ?? null);
if (!student) { if (!student) {
return { student: null, promo: null }; return { student: null, promo: null };
} }
const promoQuery = "select * from promotions where id = ?"; const promo = await db
const promo: Promotion | undefined = database.prepare(promoQuery).get( .select()
student.promotionId, .from(promotions)
); .where(eq(promotions.id, student.promotionId!))
.limit(1)
.then((rows) => rows[0] ?? null);
return { student, promo: promo ?? null }; return { student, promo };
} }
/** async function getAll(): Promise<
* Gets itself from the database. { students: Student[]; promos: Promotion[] }
* @param database The database connexion > {
* @param userId The user ID. const rows = await db
* @returns Itself from the database. .select({
*/ userId: students.userId,
function getAll( firstName: students.firstName,
database: Database, lastName: students.lastName,
): { students: Student[]; promos: Promotion[] } { mail: students.mail,
const studentsQuery = ` promotionId: students.promotionId,
select userId, firstName, lastName, mail, promotionId })
from students inner join promotions .from(students)
on students.promotionId = promotions.id .innerJoin(promotions, eq(students.promotionId, promotions.id))
where promotions.current < 6`; .where(lt(promotions.current, 6));
const students: Student[] = database.prepare(studentsQuery).all();
const promosQuery = "select * from promotions where promotions.current < 6"; const promos = await db
const promos: Promotion[] | undefined = database.prepare(promosQuery).all(); .select()
.from(promotions)
.where(lt(promotions.current, 6));
return { students, promos }; return { students: rows as Student[], promos };
} }
/** async function addStudents(
* Add users to the database. studentList: Student[],
* @param database The database connexion promoId: number,
* @param students The students to add ): Promise<void> {
* @param promoId The promotion id. for (const student of studentList) {
*/ await db
function addStudents(database: Database, students: Student[], promoId: string) { .insert(students)
const query = ` .values({
INSERT INTO students userId: student.userId,
(userId, firstName, lastName, mail, promotionId) firstName: student.firstName,
VALUES (?, ?, ?, ?, ?)`; lastName: student.lastName,
mail: student.mail,
const statement = database.prepare(query); promotionId: promoId,
})
for (const student of students) { .onConflictDoNothing();
statement.run(
student.userId,
student.firstName,
student.lastName,
student.mail,
promoId,
);
} }
} }
export const handler: Handlers<null, AuthenticatedState> = { 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( async GET(
_request: Request, _request: Request,
context: FreshContext<AuthenticatedState>, context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
using connection = connect("students");
const database = connection.database;
if (context.state.session.eduPersonPrimaryAffiliation == "student") { if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response( return new Response(
JSON.stringify(getItself(database, context.state.session.uid)), JSON.stringify(await getItself(context.state.session.uid)),
{ { headers: { "content-type": "application/json" } },
headers: {
"content-type": "application/json",
},
},
); );
} }
return new Response( return new Response(
JSON.stringify(getAll(database)), JSON.stringify(await getAll()),
{ { headers: { "content-type": "application/json" } },
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( async POST(
request: Request, request: Request,
_context: FreshContext<AuthenticatedState>, _context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
const { students, promo }: { students: Student[]; promo: string } = const { students: studentList, promo }: {
await request.json(); 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 }); return new Response(null, { status: 400 });
} }
using connection = connect("students");
const database = connection.database;
const { endyear, current } = promo.match( const { endyear, current } = promo.match(
/^(?<endyear>\d{4})-(?<current>\d)A$/, /^(?<endyear>\d{4})-(?<current>\d)A$/,
)?.groups!; )?.groups!;
database.prepare( await db
"insert or ignore into promotions (endyear, current) values (?, ?)", .insert(promotions)
).run(endyear, current); .values({ endyear: Number(endyear), current: Number(current) })
.onConflictDoNothing();
const { id: promoId }: { id: string } = database const promo_row = await db
.prepare("select id from promotions where endyear = ? and current = ?") .select()
.get(endyear, current)!; .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 }); return new Response(null, { status: 201 });
}, },