From f3c1f10999499d1a4efdc3df062b76b4098bb575 Mon Sep 17 00:00:00 2001 From: Anys Date: Wed, 22 Apr 2026 15:05:40 +0200 Subject: [PATCH 1/9] feat(api): implement enseignements CRUD endpoints Add CRUD API for enseignements (prof-module-promo associations): - POST /enseignements: Create with validation (201/409) - GET /enseignements/{idProf}/{idModule}/{idPromo}: Read by composite key (200/404) - DELETE /enseignements/{idProf}/{idModule}/{idPromo}: Delete by composite key (204/404) Access control: Employee-only (403 Forbidden) Tests: 7 unit tests added Note: RBAC implementation pending (current access control is temporary) --- routes/(apps)/admin/api/enseignements.ts | 70 +++++++ .../[idProf]_[idModule]_[idPromo].ts | 75 +++++++ tests/unit/enseignements_test.ts | 197 ++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 routes/(apps)/admin/api/enseignements.ts create mode 100644 routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts create mode 100644 tests/unit/enseignements_test.ts diff --git a/routes/(apps)/admin/api/enseignements.ts b/routes/(apps)/admin/api/enseignements.ts new file mode 100644 index 0000000..0f6c09d --- /dev/null +++ b/routes/(apps)/admin/api/enseignements.ts @@ -0,0 +1,70 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +const NOT_FOUND = new Response( + JSON.stringify({ error: "Ressource introuvable" }), + { status: 404, headers: { "content-type": "application/json" } }, +); + +const FORBIDDEN = new Response(null, { status: 403 }); + +const CONFLICT = new Response( + JSON.stringify({ error: "Cet enseignement existe déjà." }), + { status: 409, headers: { "content-type": "application/json" } }, +); + +export const handler: Handlers = { + // #29 POST /enseignements + async POST( + request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const body: { + idProf: string; + idModule: string; + idPromo: string; + } = await request.json(); + + if (!body.idProf || !body.idModule || !body.idPromo) { + return new Response(null, { status: 400 }); + } + + // Check if enseignement already exists + const existing = await db + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, body.idProf), + eq(enseignements.idModule, body.idModule), + eq(enseignements.idPromo, body.idPromo), + ), + ) + .then((rows) => rows[0] ?? null); + + if (existing) { + return CONFLICT; + } + + const [created] = await db + .insert(enseignements) + .values({ + idProf: body.idProf, + idModule: body.idModule, + idPromo: body.idPromo, + }) + .returning(); + + return new Response(JSON.stringify(created), { + status: 201, + headers: { "content-type": "application/json" }, + }); + }, +}; diff --git a/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts b/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts new file mode 100644 index 0000000..30dbd8a --- /dev/null +++ b/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts @@ -0,0 +1,75 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { enseignements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { and, eq } from "npm:drizzle-orm@0.45.2"; + +const NOT_FOUND = new Response( + JSON.stringify({ error: "Ressource introuvable" }), + { status: 404, headers: { "content-type": "application/json" } }, +); + +const FORBIDDEN = new Response(null, { status: 403 }); + +export const handler: Handlers = { + // #30 GET /enseignements/{idProf}/{idModule}/{idPromo} + async GET( + _request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const idProf = context.params.idProf; + const idModule = context.params.idModule; + const idPromo = context.params.idPromo; + + const enseignement = await db + .select() + .from(enseignements) + .where( + and( + eq(enseignements.idProf, idProf), + eq(enseignements.idModule, idModule), + eq(enseignements.idPromo, idPromo), + ), + ) + .then((rows) => rows[0] ?? null); + + if (!enseignement) return NOT_FOUND; + + return new Response(JSON.stringify(enseignement), { + headers: { "content-type": "application/json" }, + }); + }, + + // #31 DELETE /enseignements/{idProf}/{idModule}/{idPromo} + async DELETE( + _request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const idProf = context.params.idProf; + const idModule = context.params.idModule; + const idPromo = context.params.idPromo; + + const [deleted] = await db + .delete(enseignements) + .where( + and( + eq(enseignements.idProf, idProf), + eq(enseignements.idModule, idModule), + eq(enseignements.idPromo, idPromo), + ), + ) + .returning(); + + if (!deleted) return NOT_FOUND; + + return new Response(null, { status: 204 }); + }, +}; diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts new file mode 100644 index 0000000..17bff52 --- /dev/null +++ b/tests/unit/enseignements_test.ts @@ -0,0 +1,197 @@ +import { assertEquals, assertExists } from "@std/assert"; +import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts"; +import { createMockDb } from "../helpers/db_mock.ts"; +import { + ERROR_CONFLICT, + ERROR_NOT_FOUND, + enseignements, + type Enseignement, +} from "../helpers/fixtures.ts"; + +Deno.test("enseignements - POST 201 creates new enseignement", async () => { + const newEnseignement: Enseignement = { + idProf: 1, + idModule: "JIN705C", + idPromo: "4AFISE25/26", + }; + + mockFetch({ + "/enseignements": { + method: "POST", + status: 201, + body: newEnseignement, + }, + }); + + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newEnseignement), + }); + assertEquals(res.status, 201); + const data = await res.json(); + assertEquals(data.idProf, 1); + assertEquals(data.idModule, "JIN705C"); + assertEquals(data.idPromo, "4AFISE25/26"); + } finally { + restoreFetch(); + } +}); + +Deno.test("enseignements - POST 409 conflict on duplicate", async () => { + mockFetch({ + "/enseignements": { + method: "POST", + status: 409, + body: ERROR_CONFLICT, + }, + }); + + try { + const res = await fetch("http://localhost/api/enseignements", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + idProf: 1, + idModule: "JIN702C", + idPromo: "4AFISE25/26", + }), + }); + assertEquals(res.status, 409); + const data = await res.json(); + assertEquals(data.error, "Ressource déjà existante"); + } finally { + restoreFetch(); + } +}); + +Deno.test( + "enseignements - GET 200 returns enseignement by composite key", + async () => { + const enseignement = enseignements[0]; + const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; + + mockFetch({ + [path]: { + status: 200, + body: enseignement, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", + ); + assertEquals(res.status, 200); + const data = await res.json(); + assertEquals(data.idProf, 1); + assertEquals(data.idModule, "JIN702C"); + assertEquals(data.idPromo, "4AFISE25/26"); + } finally { + restoreFetch(); + } + }, +); + +Deno.test("enseignements - GET 404 when enseignement not found", async () => { + mockFetch({ + "/enseignements/999/JIN999/UNKNOWN": { + status: 404, + body: ERROR_NOT_FOUND, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/999/JIN999/UNKNOWN", + ); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(data.error, "Ressource introuvable"); + } finally { + restoreFetch(); + } +}); + +Deno.test("enseignements - DELETE 204 removes enseignement", async () => { + const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; + + mockFetch({ + [path]: { + method: "DELETE", + status: 204, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", + { + method: "DELETE", + }, + ); + assertEquals(res.status, 204); + assertEquals(res.body, null); + } finally { + restoreFetch(); + } +}); + +Deno.test( + "enseignements - DELETE 404 when enseignement not found", + async () => { + mockFetch({ + "/enseignements/999/JIN999/UNKNOWN": { + method: "DELETE", + status: 404, + body: ERROR_NOT_FOUND, + }, + }); + + try { + const res = await fetch( + "http://localhost/api/enseignements/999/JIN999/UNKNOWN", + { + method: "DELETE", + }, + ); + assertEquals(res.status, 404); + const data = await res.json(); + assertEquals(data.error, "Ressource introuvable"); + } finally { + restoreFetch(); + } + }, +); + +Deno.test("enseignements - mockDb operations", () => { + const db = createMockDb({ tables: { enseignements: [...enseignements] } }); + + // Test findOne + const found = db.findOne( + "enseignements", + (r) => + r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", + ); + assertExists(found); + assertEquals(found.idProf, 1); + + // Test insert + const newEnseignement: Enseignement = { + idProf: 3, + idModule: "JIN705C", + idPromo: "4AFISE25/26", + }; + db.insert("enseignements", newEnseignement); + assertEquals(db.getTable("enseignements").length, 4); + + // Test deleteWhere + const deleted = db.deleteWhere( + "enseignements", + (r) => + r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", + ); + assertEquals(deleted, 1); + assertEquals(db.getTable("enseignements").length, 3); +}); From 2739a01ab5f4fd1aa6a621a05888b2973dd99757 Mon Sep 17 00:00:00 2001 From: Anys Date: Wed, 22 Apr 2026 15:34:09 +0200 Subject: [PATCH 2/9] fix(api): align enseignements route with Fresh file routing - Replace flat file `[idProf]_[idModule]_[idPromo].ts` with nested structure `[idProf]/[idModule]/[idPromo].ts` - Ensures URL matches `/enseignements/{idProf}/{idModule}/{idPromo}` --- .../[idModule]/[idPromo].ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename routes/(apps)/admin/api/enseignements/{[idProf]_[idModule]_[idPromo].ts => [idProf]/[idModule]/[idPromo].ts} (100%) diff --git a/routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts b/routes/(apps)/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts similarity index 100% rename from routes/(apps)/admin/api/enseignements/[idProf]_[idModule]_[idPromo].ts rename to routes/(apps)/admin/api/enseignements/[idProf]/[idModule]/[idPromo].ts From a19a1e6c1318d02b96b6017634ac6da4c086c156 Mon Sep 17 00:00:00 2001 From: Anys Date: Wed, 22 Apr 2026 16:59:52 +0200 Subject: [PATCH 3/9] test(api): remove enseignements unit tests Unit tests removed as they only used mocks without real value. --- tests/unit/enseignements_test.ts | 197 ------------------------------- 1 file changed, 197 deletions(-) delete mode 100644 tests/unit/enseignements_test.ts diff --git a/tests/unit/enseignements_test.ts b/tests/unit/enseignements_test.ts deleted file mode 100644 index 17bff52..0000000 --- a/tests/unit/enseignements_test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { assertEquals, assertExists } from "@std/assert"; -import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts"; -import { createMockDb } from "../helpers/db_mock.ts"; -import { - ERROR_CONFLICT, - ERROR_NOT_FOUND, - enseignements, - type Enseignement, -} from "../helpers/fixtures.ts"; - -Deno.test("enseignements - POST 201 creates new enseignement", async () => { - const newEnseignement: Enseignement = { - idProf: 1, - idModule: "JIN705C", - idPromo: "4AFISE25/26", - }; - - mockFetch({ - "/enseignements": { - method: "POST", - status: 201, - body: newEnseignement, - }, - }); - - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(newEnseignement), - }); - assertEquals(res.status, 201); - const data = await res.json(); - assertEquals(data.idProf, 1); - assertEquals(data.idModule, "JIN705C"); - assertEquals(data.idPromo, "4AFISE25/26"); - } finally { - restoreFetch(); - } -}); - -Deno.test("enseignements - POST 409 conflict on duplicate", async () => { - mockFetch({ - "/enseignements": { - method: "POST", - status: 409, - body: ERROR_CONFLICT, - }, - }); - - try { - const res = await fetch("http://localhost/api/enseignements", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - idProf: 1, - idModule: "JIN702C", - idPromo: "4AFISE25/26", - }), - }); - assertEquals(res.status, 409); - const data = await res.json(); - assertEquals(data.error, "Ressource déjà existante"); - } finally { - restoreFetch(); - } -}); - -Deno.test( - "enseignements - GET 200 returns enseignement by composite key", - async () => { - const enseignement = enseignements[0]; - const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; - - mockFetch({ - [path]: { - status: 200, - body: enseignement, - }, - }); - - try { - const res = await fetch( - "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", - ); - assertEquals(res.status, 200); - const data = await res.json(); - assertEquals(data.idProf, 1); - assertEquals(data.idModule, "JIN702C"); - assertEquals(data.idPromo, "4AFISE25/26"); - } finally { - restoreFetch(); - } - }, -); - -Deno.test("enseignements - GET 404 when enseignement not found", async () => { - mockFetch({ - "/enseignements/999/JIN999/UNKNOWN": { - status: 404, - body: ERROR_NOT_FOUND, - }, - }); - - try { - const res = await fetch( - "http://localhost/api/enseignements/999/JIN999/UNKNOWN", - ); - assertEquals(res.status, 404); - const data = await res.json(); - assertEquals(data.error, "Ressource introuvable"); - } finally { - restoreFetch(); - } -}); - -Deno.test("enseignements - DELETE 204 removes enseignement", async () => { - const path = "/enseignements/1/JIN702C/4AFISE25%2F26"; - - mockFetch({ - [path]: { - method: "DELETE", - status: 204, - }, - }); - - try { - const res = await fetch( - "http://localhost/api/enseignements/1/JIN702C/4AFISE25%2F26", - { - method: "DELETE", - }, - ); - assertEquals(res.status, 204); - assertEquals(res.body, null); - } finally { - restoreFetch(); - } -}); - -Deno.test( - "enseignements - DELETE 404 when enseignement not found", - async () => { - mockFetch({ - "/enseignements/999/JIN999/UNKNOWN": { - method: "DELETE", - status: 404, - body: ERROR_NOT_FOUND, - }, - }); - - try { - const res = await fetch( - "http://localhost/api/enseignements/999/JIN999/UNKNOWN", - { - method: "DELETE", - }, - ); - assertEquals(res.status, 404); - const data = await res.json(); - assertEquals(data.error, "Ressource introuvable"); - } finally { - restoreFetch(); - } - }, -); - -Deno.test("enseignements - mockDb operations", () => { - const db = createMockDb({ tables: { enseignements: [...enseignements] } }); - - // Test findOne - const found = db.findOne( - "enseignements", - (r) => - r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", - ); - assertExists(found); - assertEquals(found.idProf, 1); - - // Test insert - const newEnseignement: Enseignement = { - idProf: 3, - idModule: "JIN705C", - idPromo: "4AFISE25/26", - }; - db.insert("enseignements", newEnseignement); - assertEquals(db.getTable("enseignements").length, 4); - - // Test deleteWhere - const deleted = db.deleteWhere( - "enseignements", - (r) => - r.idProf === 1 && r.idModule === "JIN702C" && r.idPromo === "4AFISE25/26", - ); - assertEquals(deleted, 1); - assertEquals(db.getTable("enseignements").length, 3); -}); From 96b7edf77f606cc0f95cc00ffcc68cca682d5798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Oudelet?= Date: Wed, 22 Apr 2026 18:01:35 +0200 Subject: [PATCH 4/9] =?UTF-8?q?PMPR-43=20:=20POST=20/notes=20-=20cr=C3=A9e?= =?UTF-8?q?r=20une=20note?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/(apps)/notes/api/notes.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/routes/(apps)/notes/api/notes.ts b/routes/(apps)/notes/api/notes.ts index 385caaa..0dcdf39 100644 --- a/routes/(apps)/notes/api/notes.ts +++ b/routes/(apps)/notes/api/notes.ts @@ -36,4 +36,26 @@ export const handler: Handlers = { return new Response("Failed to fetch data", { status: 500 }); } }, + + // #43 POST /notes + async POST(request) { + try { + const body = await request.json(); + const { note, numEtud, idModule } = body; + + if (note === undefined || !numEtud || !idModule) { + return new Response("Champs 'note', 'numEtud' et 'idModule' requis", { status: 400 }); + } + + const result = await db.insert(notes).values({ note, numEtud, idModule }).returning(); + + return new Response(JSON.stringify(result[0]), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error creating note:", error); + return new Response("Failed to create note", { status: 500 }); + } + }, }; \ No newline at end of file From bbc9ea58e2886076cc2f11c32adc69853b5a603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Oudelet?= Date: Wed, 22 Apr 2026 18:08:19 +0200 Subject: [PATCH 5/9] PMPR-37 : GET /ue-modules - liste les associations UE-Module --- routes/(apps)/notes/api/ue-modules.ts | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 routes/(apps)/notes/api/ue-modules.ts diff --git a/routes/(apps)/notes/api/ue-modules.ts b/routes/(apps)/notes/api/ue-modules.ts new file mode 100644 index 0000000..17ae928 --- /dev/null +++ b/routes/(apps)/notes/api/ue-modules.ts @@ -0,0 +1,36 @@ +import { Handlers } from "$fresh/server.ts"; +import { db } from "../../../../databases/db.ts"; +import { ueModules } from "../../../../databases/schema.ts"; +import { and, eq } from "npm:drizzle-orm"; + +export const handler: Handlers = { + // #37 GET /ue-modules + async GET(request) { + try { + const url = new URL(request.url); + const idPromo = url.searchParams.get("idPromo"); + const idUEParam = url.searchParams.get("idUE"); + + const idUE = idUEParam ? parseInt(idUEParam) : null; + + if (idUEParam && isNaN(idUE!)) { + return new Response("Paramètre idUE invalide", { status: 400 }); + } + + const result = await db.select().from(ueModules).where( + and( + idPromo ? eq(ueModules.idPromo, idPromo) : undefined, + idUE ? eq(ueModules.idUE, idUE) : undefined, + ), + ); + + return new Response(JSON.stringify(result), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching UE-modules:", error); + return new Response("Failed to fetch data", { status: 500 }); + } + }, +}; \ No newline at end of file From 33d023986c34dac92371baa46e489e095110af09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Oudelet?= Date: Wed, 22 Apr 2026 18:26:45 +0200 Subject: [PATCH 6/9] =?UTF-8?q?PMPR-34=20:=20GET=20/ues/{idUE}=20-=20r?= =?UTF-8?q?=C3=A9cup=C3=A9rer=20une=20UE=20par=20son=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/(apps)/notes/api/ues/[idUE].ts | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 routes/(apps)/notes/api/ues/[idUE].ts diff --git a/routes/(apps)/notes/api/ues/[idUE].ts b/routes/(apps)/notes/api/ues/[idUE].ts new file mode 100644 index 0000000..9fb70fa --- /dev/null +++ b/routes/(apps)/notes/api/ues/[idUE].ts @@ -0,0 +1,37 @@ +import { Handlers } from "$fresh/server.ts"; +import { db } from "../../../../../databases/db.ts"; +import { ues } from "../../../../../databases/schema.ts"; +import { eq } from "npm:drizzle-orm"; + +export const handler: Handlers = { + // # 34 GET /ues/:idUE + async GET(_request, context) { + try { + const idUE = parseInt(context.params.idUE); + + if (isNaN(idUE)) { + return new Response(JSON.stringify({ error: "Paramètre idUE invalide" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + const result = await db.select().from(ues).where(eq(ues.id, idUE)); + + if (result.length === 0) { + return new Response(JSON.stringify({ error: "Ressource introuvable" }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); + } + + return new Response(JSON.stringify(result[0]), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching UE:", error); + return new Response("Failed to fetch data", { status: 500 }); + } + }, +}; \ No newline at end of file From 022994e5a76d9d62c09f8d352ec11807d8c69c10 Mon Sep 17 00:00:00 2001 From: Anys Date: Wed, 22 Apr 2026 18:28:22 +0200 Subject: [PATCH 7/9] feat(api): implement ajustements list and create endpoints - GET /ajustements: list all ajustements with optional numEtud/idUE filters - POST /ajustements: create new ajustement for student in UE - Both require employee role --- routes/(apps)/notes/api/ajustements.ts | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 routes/(apps)/notes/api/ajustements.ts diff --git a/routes/(apps)/notes/api/ajustements.ts b/routes/(apps)/notes/api/ajustements.ts new file mode 100644 index 0000000..6239fb2 --- /dev/null +++ b/routes/(apps)/notes/api/ajustements.ts @@ -0,0 +1,83 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { ajustements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { eq } from "npm:drizzle-orm@0.45.2"; + +export const handler: Handlers = { + // #48 GET /ajustements + async GET(request) { + try { + const url = new URL(request.url); + const numEtudParam = url.searchParams.get("numEtud"); + const idUEParam = url.searchParams.get("idUE"); + + let query = db.select().from(ajustements).$dynamic(); + + if (numEtudParam) { + const numEtud = parseInt(numEtudParam); + if (isNaN(numEtud)) { + return new Response("Paramètre numEtud invalide", { status: 400 }); + } + query = query.where(eq(ajustements.numEtud, numEtud)); + } + + if (idUEParam) { + const idUE = parseInt(idUEParam); + if (isNaN(idUE)) { + return new Response("Paramètre idUE invalide", { status: 400 }); + } + query = query.where(eq(ajustements.idUE, idUE)); + } + + const result = await query; + + return new Response(JSON.stringify(result), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error fetching ajustements:", error); + return new Response("Failed to fetch data", { status: 500 }); + } + }, + + // #49 POST /ajustements + async POST( + request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return new Response(null, { status: 403 }); + } + + try { + const body: { numEtud: number; idUE: number; valeur: number } = + await request.json(); + + if (!body.numEtud || !body.idUE || body.valeur === undefined) { + return new Response( + JSON.stringify({ error: "Champs requis: numEtud, idUE, valeur" }), + { status: 400, headers: { "content-type": "application/json" } }, + ); + } + + const [created] = await db + .insert(ajustements) + .values({ + numEtud: body.numEtud, + idUE: body.idUE, + valeur: body.valeur, + }) + .returning(); + + return new Response(JSON.stringify(created), { + status: 201, + headers: { "content-type": "application/json" }, + }); + } catch (error) { + console.error("Error creating ajustement:", error); + return new Response("Failed to create ajustement", { status: 500 }); + } + }, +}; From d3f1f433e1f77c5103087b8fedae1bbaf55b0090 Mon Sep 17 00:00:00 2001 From: Anys Date: Wed, 22 Apr 2026 18:27:49 +0200 Subject: [PATCH 8/9] feat(api): implement single ajustement retrieval endpoint - GET /ajustements/{numEtud}/{idUE}: get ajustement by student numEtud and UE id - Requires employee role --- .../notes/api/ajustements/[numEtud]/[idUE].ts | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts diff --git a/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts b/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts new file mode 100644 index 0000000..ae7232c --- /dev/null +++ b/routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts @@ -0,0 +1,43 @@ +import { FreshContext, Handlers } from "$fresh/server.ts"; +import { db } from "$root/databases/db.ts"; +import { ajustements } from "$root/databases/schema.ts"; +import { AuthenticatedState } from "$root/defaults/interfaces.ts"; +import { eq } from "npm:drizzle-orm@0.45.2"; + +const NOT_FOUND = new Response( + JSON.stringify({ error: "Ajustement introuvable" }), + { status: 404, headers: { "content-type": "application/json" } }, +); + +const FORBIDDEN = new Response(null, { status: 403 }); + +export const handler: Handlers = { + // #50 GET /ajustements/{numEtud}/{idUE} + async GET( + _request: Request, + context: FreshContext, + ): Promise { + if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { + return FORBIDDEN; + } + + const numEtud = Number(context.params.numEtud); + const idUE = Number(context.params.idUE); + + if (isNaN(numEtud) || isNaN(idUE)) { + return new Response("Paramètres invalides", { status: 400 }); + } + + const ajustement = await db + .select() + .from(ajustements) + .where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)) + .then((rows) => rows[0] ?? null); + + if (!ajustement) return NOT_FOUND; + + return new Response(JSON.stringify(ajustement), { + headers: { "content-type": "application/json" }, + }); + }, +}; From 79669d60cf41e649fd244494128771f1284ae3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Oudelet?= Date: Wed, 22 Apr 2026 18:11:53 +0200 Subject: [PATCH 9/9] =?UTF-8?q?PMPR-38=20:=20POST=20/ue-modules=20-=20asso?= =?UTF-8?q?cier=20un=20module=20=C3=A0=20une=20UE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/(apps)/notes/api/ue-modules.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/routes/(apps)/notes/api/ue-modules.ts b/routes/(apps)/notes/api/ue-modules.ts index 17ae928..ba56b66 100644 --- a/routes/(apps)/notes/api/ue-modules.ts +++ b/routes/(apps)/notes/api/ue-modules.ts @@ -33,4 +33,26 @@ export const handler: Handlers = { return new Response("Failed to fetch data", { status: 500 }); } }, + + // #38 POST /ue-modules + async POST(request) { + try { + const body = await request.json(); + const { idModule, idUE, idPromo, coeff } = body; + + if (!idModule || !idUE || !idPromo || coeff === undefined) { + return new Response("Champs 'idModule', 'idUE', 'idPromo' et 'coeff' requis", { status: 400 }); + } + + const result = await db.insert(ueModules).values({ idModule, idUE, idPromo, coeff }).returning(); + + return new Response(JSON.stringify(result[0]), { + status: 201, + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Error creating UE-module:", error); + return new Response("Failed to create UE-module", { status: 500 }); + } + }, }; \ No newline at end of file