fix(lint): add version to drizzle-orm imports and prefix unused NOT_FOUND

This commit is contained in:
2026-04-26 00:24:27 +02:00
committed by djalim
parent cdd9c0bf06
commit 6db04045f4
20 changed files with 880 additions and 82 deletions
+74
View File
@@ -0,0 +1,74 @@
name: "Tests"
on:
pull_request:
branches:
- main
- develop
push:
branches:
- develop
jobs:
unit:
name: "Unit tests"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Run unit tests
run: deno task test:unit
integration:
name: "Integration tests"
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: polympr_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Install drizzle-kit
run: npm install --ignore-scripts
- name: Apply migrations
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task migrate
- name: Run integration tests
env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: test
POSTGRES_PASS: test
POSTGRES_DB: polympr_test
run: deno task test:integration
+338
View File
@@ -0,0 +1,338 @@
# PolyMPR - Claude Code Context
## 📋 Project Overview
**PolyMPR** (Poly Management Platform for Resources) is a modular HR management
system built with **Deno + Fresh** framework. It's designed to help
organizations manage HR, student records, notes, mobility programs, and
role-based administration.
### Stack
- **Runtime**: Deno
- **Framework**: Fresh (edge-ready web framework)
- **Database**: PostgreSQL with Drizzle ORM
- **Frontend**: Preact with signals
- **Authentication**: JWT-based via cookies
- **Testing**: Deno test framework with HappyDOM for DOM testing
### Current Status
🚧 **In Progress** - Application is far from complete. The schema below is the
**final/definitive schema** that should guide all development.
---
## 🏗️ Architecture
### Module Structure
The application uses a **modulith architecture** with the following modules:
```
routes/(apps)/
├── students/ - Student management & promotions
├── notes/ - Grade management & academic records
├── mobility/ - Mobility programs & exchanges
└── admin/ - Role & permission management
```
### Key Directories
- `/routes` - Fresh routes and components
- `/databases` - Database connection, schema, and migrations
- `/defaults` - Interfaces and shared types
- `/tests` - Unit, integration, and E2E tests
- `/static` - Public assets
### Authentication Flow
1. User authenticates via CAS (Polytech)
2. JWT token stored in `sessionToken` cookie
3. Middleware validates token on each request
4. Public routes: `/`, `/login`, `/logout`, `/about`, `/contact`
5. All other routes require authentication
---
## 📊 Database Schema (Final/Definitive)
```mermaid
erDiagram
USER {
string id PK
string nom
string prenom
int idRole FK
}
ROLE {
int id PK
string nom
}
PERMISSION {
int id PK
string nom
}
ROLE_PERMISSION {
int idRole PK,FK
int idPermission PK,FK
}
STUDENT {
int numEtud PK
string nom
string prenom
string idPromo FK
}
PROMOTION {
string idPromo PK
string annee
}
MODULE {
string id PK
string nom
}
ENSEIGNEMENT {
string idProf PK,FK
string idModule PK,FK
string idPromo PK,FK
}
UE {
int id PK
string nom
}
UE_MODULE {
string idModule PK,FK
int idUE PK,FK
string idPromo PK,FK
float coeff
}
NOTE {
int numEtud PK,FK
string idModule PK,FK
float note
}
AJUSTEMENT {
int numEtud PK,FK
int idUE PK,FK
float valeur
}
USER }o--|| ROLE : "a"
ROLE_PERMISSION }o--|| ROLE : "accorde"
ROLE_PERMISSION }o--|| PERMISSION : "inclut"
ENSEIGNEMENT }o--|| USER : "réalisé par"
ENSEIGNEMENT }o--|| MODULE : "porte sur"
ENSEIGNEMENT }o--|| PROMOTION : "concerne"
STUDENT }o--|| PROMOTION : "appartient à"
UE_MODULE }o--|| MODULE : "associe"
UE_MODULE }o--|| UE : "appartient à"
UE_MODULE }o--|| PROMOTION : "pour"
NOTE }o--|| STUDENT : "reçoit"
NOTE }o--|| MODULE : "dans"
AJUSTEMENT }o--|| STUDENT : "concerne"
AJUSTEMENT }o--|| UE : "dans"
```
### Current Schema (Incomplete)
The current Drizzle ORM schema in `/databases/schema.ts` only implements:
- `promotions`
- `students`
- `mobility`
**Migration needed**: Update schema to match the final ER diagram above.
---
## 🎯 Open Issues (69 total)
### UI Pages
**Catalog**
- 📋 UI - Page Catalogue d'applications (#71)
**Components**
- 🎨 UI (composant) - Popup Résultats d'import (#75)
**Students**
- 📋 UI - Admin Liste des élèves (#79)
- 📋 UI - Admin Gestion des promotions (#80)
- 📋 UI - Admin Import xlsx élèves (#81)
- 📋 UI - Admin Édition d'un élève (#82)
**Notes**
- 📋 UI - Page Élève Mes Notes (#72)
- 📋 UI - Admin Consulter les notes (#73)
- 📋 UI - Admin Importer des notes (.xlsx) (#74)
- 📋 UI - Admin Édition notes d'un élève (#76)
- 📋 UI - Admin Récap notes élève / semestre (#77)
- 📋 UI - Admin Gestion des UEs (#78)
**Administration**
- 📋 UI - Gestion des utilisateurs (#83)
- 📋 UI - Gestion des rôles (#84)
- 📋 UI - Permissions d'un rôle (#85)
- 📋 UI - Vue des permissions (#86)
- 📋 UI - Gestion des modules (#87)
- 📋 UI - Enseignements (Assignations) (#88)
---
### API Endpoints
**Students API**
- 📋 GET `/students` (#7)
- 📋 POST `/students` (#8)
- 📋 POST `/students/import-csv` (#9)
- 📋 GET `/students/{numEtud}` (#10)
- 📋 PUT `/students/{numEtud}` (#11)
- 📋 DELETE `/students/{numEtud}` (#12)
- 📋 GET `/promotions` (#13)
- 📋 POST `/promotions` (#14)
- 📋 GET `/promotions/{idPromo}` (#15)
- 📋 PUT `/promotions/{idPromo}` (#16)
- 📋 DELETE `/promotions/{idPromo}` (#17)
**Administration API - Modules & Enseignements**
- 📋 GET `/modules` (#23)
- 📋 POST `/modules` (#24)
- 📋 GET `/modules/{idModule}` (#25)
- 📋 PUT `/modules/{idModule}` (#26)
- 📋 DELETE `/modules/{idModule}` (#27)
- 📋 POST `/enseignements` (#29)
- 📋 GET `/enseignements/{idProf}/{idModule}/{idPromo}` (#30)
- 📋 DELETE `/enseignements/{idProf}/{idModule}/{idPromo}` (#31)
**Notes API - UEs & UE-Modules**
- 📋 GET `/ues` (#32)
- 📋 POST `/ues` (#33)
- 📋 GET `/ues/{idUE}` (#34)
- 📋 PUT `/ues/{idUE}` (#35)
- 📋 DELETE `/ues/{idUE}` (#36)
- 📋 GET `/ue-modules` (#37)
- 📋 POST `/ue-modules` (#38)
- 📋 GET `/ue-modules/{idModule}/{idUE}/{idPromo}` (#39)
- 📋 PUT `/ue-modules/{idModule}/{idUE}/{idPromo}` (#40)
- 📋 DELETE `/ue-modules/{idModule}/{idUE}/{idPromo}` (#41)
**Notes API - Notes & Ajustements**
- 📋 GET `/notes` (#42)
- 📋 POST `/notes` (#43)
- 📋 POST `/notes/import-xlsx` (#44)
- 📋 GET `/notes/{numEtud}/{idModule}` (#45)
- 📋 PUT `/notes/{numEtud}/{idModule}` (#46)
- 📋 DELETE `/notes/{numEtud}/{idModule}` (#47)
- 📋 GET `/ajustements` (#48)
- 📋 POST `/ajustements` (#49)
- 📋 GET `/ajustements/{numEtud}/{idUE}` (#50)
- 📋 PUT `/ajustements/{numEtud}/{idUE}` (#51)
- 📋 DELETE `/ajustements/{numEtud}/{idUE}` (#52)
**Administration API - Users, Roles & Permissions**
- 📋 GET `/users` (#60)
- 📋 POST `/users` (#61)
- 📋 GET `/users/{id}` (#62)
- 📋 PUT `/users/{id}` (#63)
- 📋 DELETE `/users/{id}` (#64)
- 📋 GET `/roles` (#65)
- 📋 POST `/roles` (#66)
- 📋 GET `/roles/{idRole}` (#67)
- 📋 PUT `/roles/{idRole}` (#68)
- 📋 DELETE `/roles/{idRole}` (#69)
- 📋 GET `/permissions` (#70)
---
## 🎨 Design Reference
**Figma Prototype**:
https://www.figma.com/design/La79bsUsWnJCtMsrrt2zGd/Prototype?node-id=0-1
This is the **final design specification** for the UI. All UI implementations
should follow this design.
---
## 🚀 Development Guidelines
### Getting Started
```bash
# Run tests
deno task test
# Start development server
deno task start
# Build for production
deno task build
# Format & lint
deno task check
```
### Git Workflow
1. Create branch: `git checkout -b PMPR-{ISSUE_ID}`
2. Implement changes
3. Run tests and linting
4. Submit PR
### Code Style
- Format: Follow Deno defaults (enforced via `deno fmt`)
- Linting: Fresh recommended rules
- TypeScript strict mode enabled
- Use Drizzle ORM for database operations
### Testing
- Write unit tests for business logic
- Integration tests for API endpoints
- E2E tests with HappyDOM for UI interactions
- Mock database with provided helpers
---
## 📦 Key Dependencies
- **fresh@1.7.3** - Web framework
- **drizzle-orm@0.45.2** - ORM
- **pg@8.20.0** - PostgreSQL driver
- **@popov/jwt@1.0.1** - JWT utilities
- **preact@10.22.0** - UI library
- **happy-dom@16.0.0** - DOM testing
---
## 🔗 Related Resources
- **Repository**: https://git.polytech.djalim.fr/djalim/PolyMPR
- **Issue Tracker**: Gitea (via `tea` CLI)
- **Wiki**: Check CONTRIBUTING.md for dev setup
- **Database**: PostgreSQL (configured in `.env`)
---
## 💡 Important Notes
1. **Current Limitation**: The database schema in `/databases/schema.ts` does
NOT match the final ER diagram. This is a priority migration task.
2. **Design System**: Follow the Figma prototype for all UI work.
3. **Module Pattern**: Each module should follow the same pattern: routes, API
endpoints, components, and tests.
4. **Permissions**: All admin operations should respect the ROLE_PERMISSION
system.
5. **Fresh Conventions**: Routes use Fresh's file-based routing convention
(e.g., `routes/path/index.tsx`).
+158
View File
@@ -0,0 +1,158 @@
# Bug Report — PolyMPR
> Généré le 2026-04-23
---
## 🔴 Critique
### #1 — Schema mismatch : module mobility entièrement cassé
**Fichier** : `routes/(apps)/mobility/api/insert_mobility.ts`
Références à des colonnes inexistantes dans le schéma Drizzle :
| Utilisé dans le code | Colonne réelle |
| ---------------------- | ------------------ |
| `students.userId` | `students.numEtud` |
| `students.firstName` | `students.nom` |
| `students.lastName` | `students.prenom` |
| `students.promotionId` | `students.idPromo` |
| `promotions.endyear` | `promotions.annee` |
| `promotions.current` | _(n'existe pas)_ |
Le module crashe à l'exécution. À corriger en alignant les noms de colonnes avec
le schéma.
---
### #2 — Auth manquante sur de nombreux endpoints
Les endpoints suivants n'ont aucune vérification `eduPersonPrimaryAffiliation` :
- `routes/(apps)/notes/api/notes.ts` (GET, POST)
- `routes/(apps)/notes/api/ue-modules.ts` (GET, POST)
- `routes/(apps)/notes/api/ues.ts` (GET, POST)
- `routes/(apps)/notes/api/ues/[idUE].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/users.ts` (GET, POST)
- `routes/(apps)/admin/api/users/[id].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/modules/[idModule].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/roles.ts` (GET, POST)
- `routes/(apps)/admin/api/roles/[idRole].ts` (GET, PUT, DELETE)
- `routes/(apps)/admin/api/permissions.ts` (GET)
- `routes/(apps)/mobility/api/insert_mobility.ts`
Tous ces endpoints exposent des données sensibles sans vérifier les permissions.
---
## 🟠 Haut
### #3 — Bug Drizzle ORM : `.where()` avec plusieurs `eq()` sans `and()`
**Fichier** : `routes/(apps)/notes/api/ajustements/[numEtud]/[idUE].ts` — lignes
34, 72, 100
`.where()` n'accepte qu'un seul argument. Passer plusieurs `eq()` séparés par
des virgules ne génère pas le SQL attendu (seule la première condition est prise
en compte).
```ts
// ❌ Incorrect
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
// ✅ Correct
.where(and(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE)))
```
---
### #4 — Bug Drizzle ORM : `.where()` à 3 conditions sans `and()`
**Fichier** :
`routes/(apps)/notes/api/ue-modules/[idModule]/[idUE]/[idPromo].ts` — handler
GET (~ligne 41)
Même problème que #3, mais avec 3 conditions. Les handlers PUT et DELETE ont
déjà `and()`, seul le GET est affecté.
```ts
// ❌ Incorrect
.where(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
)
// ✅ Correct
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
```
---
## 🟡 Moyen
### #5 — `and()` passé avec des valeurs `undefined`
**Fichier** : `routes/(apps)/notes/api/ue-modules.ts`
```ts
and(
idPromo ? eq(ueModules.idPromo, idPromo) : undefined,
idUE ? eq(ueModules.idUE, idUE) : undefined,
);
```
Drizzle tolère les `undefined` dans `and()` dans certaines versions, mais ce
n'est pas garanti. Mieux vaut construire les conditions dynamiquement avant de
les passer.
---
### #6 — Validation `!numEtud` rejette faussement `0`
**Fichier** : `routes/(apps)/notes/api/notes.ts` — handler POST
```ts
// ❌ Rejette numEtud = 0
if (note === undefined || !numEtud || !idModule)
// ✅ Correct
if (note === undefined || numEtud === undefined || numEtud === null || !idModule)
```
---
### #7 — `Number(idRole)` sans vérification `isNaN`
**Fichier** : `routes/(apps)/admin/api/users.ts`
Si `idRole` est une chaîne non numérique, `Number()` retourne `NaN` ce qui
provoque une erreur SQL.
```ts
// ❌ Pas de vérification
const rows = idRole
? await db.select().from(users).where(eq(users.idRole, Number(idRole)))
: await db.select().from(users);
// ✅ Valider avant usage
const role = Number(idRole);
if (isNaN(role)) return new Response(..., { status: 400 });
```
---
### #8 — Réponses d'erreur en texte brut au lieu de JSON
**Fichier** : `routes/(apps)/notes/api/notes.ts`
Certaines réponses d'erreur retournent une string sans
`content-type: application/json`, incohérent avec le reste de l'API qui retourne
`{ error: "..." }`.
+1 -3
View File
@@ -16,11 +16,9 @@ services:
image: postgres image: postgres
restart: always restart: always
shm_size: 128mb shm_size: 128mb
environment: environment:
POSTGRES_PASSWORD: ${POSTGRES_PASS} POSTGRES_PASSWORD: ${POSTGRES_PASS}
deploy: deploy:
replicas: 1 replicas: 1
placement: placement:
constraints: [node.role == manager] constraints: [node.role == manager]
+1 -1
View File
@@ -677,4 +677,4 @@
"schemas": {}, "schemas": {},
"tables": {} "tables": {}
} }
} }
+1 -1
View File
@@ -10,4 +10,4 @@
"breakpoints": true "breakpoints": true
} }
] ]
} }
Generated
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1776548001,
"narHash": "sha256-ZSK0NL4a1BwVbbTBoSnWgbJy9HeZFXLYQizjb2DPF24=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b12141ef619e0a9c1c84dc8c684040326f27cdcc",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
+62
View File
@@ -0,0 +1,62 @@
{
description = "PolyMPR CLI - A tool for managing PolyMPR modules";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages.pmpr = pkgs.stdenv.mkDerivation {
pname = "pmpr";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [
pkgs.deno
pkgs.autoPatchelfHook
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
buildPhase = ''
export HOME=$TMPDIR
deno cache toolbox/cli.ts
deno compile -A --output pmpr toolbox/cli.ts
'';
installPhase = ''
mkdir -p $out/bin
cp pmpr $out/bin/pmpr
'';
};
packages.default = self.packages.${system}.pmpr;
devShells.default = pkgs.mkShell {
nativeBuildInputs = [
pkgs.deno
pkgs.patchelf
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
export NIX_LD_INTERPRETER=$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)
echo "Welcome to PolyMPR development shell!"
echo "Use 'deno task compile' to build the CLI."
'';
};
}
);
}
+1 -1
View File
@@ -9,4 +9,4 @@
"drizzle-kit": "^0.31.10", "drizzle-kit": "^0.31.10",
"tsx": "^4.21.0" "tsx": "^4.21.0"
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ import { enseignements } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2"; import { and, eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response( const _NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }), JSON.stringify({ error: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } }, { status: 404, headers: { "content-type": "application/json" } },
); );
+1 -1
View File
@@ -2,7 +2,7 @@ import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import { db } from "$root/databases/db.ts";
import { modules } from "$root/databases/schema.ts"; import { modules } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm"; import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers<null, AuthenticatedState> = { export const handler: Handlers<null, AuthenticatedState> = {
// #23 GET /modules // #23 GET /modules
@@ -2,7 +2,7 @@ import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import { db } from "$root/databases/db.ts";
import { modules } from "$root/databases/schema.ts"; import { modules } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm"; import { eq } from "npm:drizzle-orm@0.45.2";
const NOT_FOUND = new Response( const NOT_FOUND = new Response(
JSON.stringify({ error: "Ressource introuvable" }), JSON.stringify({ error: "Ressource introuvable" }),
+7 -4
View File
@@ -1,7 +1,7 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts"; import { db } from "../../../../databases/db.ts";
import { notes } from "../../../../databases/schema.ts"; import { notes } from "../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm"; import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// #42 GET /notes // #42 GET /notes
@@ -44,10 +44,13 @@ export const handler: Handlers = {
const { note, numEtud, idModule } = body; const { note, numEtud, idModule } = body;
if (note === undefined || !numEtud || !idModule) { if (note === undefined || !numEtud || !idModule) {
return new Response("Champs 'note', 'numEtud' et 'idModule' requis", { status: 400 }); return new Response("Champs 'note', 'numEtud' et 'idModule' requis", {
status: 400,
});
} }
const result = await db.insert(notes).values({ note, numEtud, idModule }).returning(); const result = await db.insert(notes).values({ note, numEtud, idModule })
.returning();
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
status: 201, status: 201,
@@ -58,4 +61,4 @@ export const handler: Handlers = {
return new Response("Failed to create note", { status: 500 }); return new Response("Failed to create note", { status: 500 });
} }
}, },
}; };
@@ -1,20 +1,23 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../../databases/db.ts"; import { db } from "../../../../../../databases/db.ts";
import { notes } from "../../../../../../databases/schema.ts"; import { notes } from "../../../../../../databases/schema.ts";
import { and, eq } from "npm:drizzle-orm"; import { and, eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// #45 GET /notes/:numEtud/:idModule // #45 GET /notes/:numEtud/:idModule
async GET(_request, context) { async GET(_request, context) {
try { try {
const numEtud = parseInt(context.params.numEtud); const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params; const { idModule } = context.params;
if (isNaN(numEtud)) { if (isNaN(numEtud)) {
return new Response(JSON.stringify({ error: "Paramètre numEtud invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre numEtud invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const result = await db.select().from(notes).where( const result = await db.select().from(notes).where(
@@ -25,10 +28,13 @@ export const handler: Handlers = {
); );
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
@@ -48,10 +54,13 @@ export const handler: Handlers = {
const { idModule } = context.params; const { idModule } = context.params;
if (isNaN(numEtud)) { if (isNaN(numEtud)) {
return new Response(JSON.stringify({ error: "Paramètre numEtud invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre numEtud invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const body = await request.json(); const body = await request.json();
@@ -69,10 +78,13 @@ export const handler: Handlers = {
).returning(); ).returning();
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
@@ -92,10 +104,13 @@ export const handler: Handlers = {
const { idModule } = context.params; const { idModule } = context.params;
if (isNaN(numEtud)) { if (isNaN(numEtud)) {
return new Response(JSON.stringify({ error: "Paramètre numEtud invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre numEtud invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const result = await db.delete(notes).where( const result = await db.delete(notes).where(
@@ -106,10 +121,13 @@ export const handler: Handlers = {
).returning(); ).returning();
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
@@ -118,4 +136,4 @@ export const handler: Handlers = {
return new Response("Failed to delete note", { status: 500 }); return new Response("Failed to delete note", { status: 500 });
} }
}, },
}; };
+14 -6
View File
@@ -1,10 +1,10 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts"; import { db } from "../../../../databases/db.ts";
import { ueModules } from "../../../../databases/schema.ts"; import { ueModules } from "../../../../databases/schema.ts";
import { and, eq } from "npm:drizzle-orm"; import { and, eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// #37 GET /ue-modules // #37 GET /ue-modules
async GET(request) { async GET(request) {
try { try {
const url = new URL(request.url); const url = new URL(request.url);
@@ -33,7 +33,7 @@ export const handler: Handlers = {
return new Response("Failed to fetch data", { status: 500 }); return new Response("Failed to fetch data", { status: 500 });
} }
}, },
// #38 POST /ue-modules // #38 POST /ue-modules
async POST(request) { async POST(request) {
try { try {
@@ -41,10 +41,18 @@ export const handler: Handlers = {
const { idModule, idUE, idPromo, coeff } = body; const { idModule, idUE, idPromo, coeff } = body;
if (!idModule || !idUE || !idPromo || coeff === undefined) { if (!idModule || !idUE || !idPromo || coeff === undefined) {
return new Response("Champs 'idModule', 'idUE', 'idPromo' et 'coeff' requis", { status: 400 }); return new Response(
"Champs 'idModule', 'idUE', 'idPromo' et 'coeff' requis",
{ status: 400 },
);
} }
const result = await db.insert(ueModules).values({ idModule, idUE, idPromo, coeff }).returning(); const result = await db.insert(ueModules).values({
idModule,
idUE,
idPromo,
coeff,
}).returning();
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
status: 201, status: 201,
@@ -55,4 +63,4 @@ export const handler: Handlers = {
return new Response("Failed to create UE-module", { status: 500 }); return new Response("Failed to create UE-module", { status: 500 });
} }
}, },
}; };
@@ -91,14 +91,17 @@ export const handler: Handlers<null, AuthenticatedState> = {
if (!updated) return NOT_FOUND; if (!updated) return NOT_FOUND;
return new Response(JSON.stringify({ return new Response(
idModule: updated.idModule, JSON.stringify({
idUE: updated.idUE, idModule: updated.idModule,
idPromo: updated.idPromo, idUE: updated.idUE,
coeff: updated.coeff, idPromo: updated.idPromo,
}), { coeff: updated.coeff,
headers: { "content-type": "application/json" }, }),
}); {
headers: { "content-type": "application/json" },
},
);
}, },
// #41 DELETE /ue-modules/{idModule}/{idUE}/{idPromo} // #41 DELETE /ue-modules/{idModule}/{idUE}/{idPromo}
+1 -1
View File
@@ -39,4 +39,4 @@ export const handler: Handlers = {
return new Response("Failed to create UE", { status: 500 }); return new Response("Failed to create UE", { status: 500 });
} }
}, },
}; };
+46 -27
View File
@@ -1,28 +1,34 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../databases/db.ts"; import { db } from "../../../../../databases/db.ts";
import { ues } from "../../../../../databases/schema.ts"; import { ues } from "../../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm"; import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// # 34 GET /ues/:idUE // # 34 GET /ues/:idUE
async GET(_request, context) { async GET(_request, context) {
try { try {
const idUE = parseInt(context.params.idUE); const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) { if (isNaN(idUE)) {
return new Response(JSON.stringify({ error: "Paramètre idUE invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre idUE invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const result = await db.select().from(ues).where(eq(ues.id, idUE)); const result = await db.select().from(ues).where(eq(ues.id, idUE));
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
@@ -41,10 +47,13 @@ export const handler: Handlers = {
const idUE = parseInt(context.params.idUE); const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) { if (isNaN(idUE)) {
return new Response(JSON.stringify({ error: "Paramètre idUE invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre idUE invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const body = await request.json(); const body = await request.json();
@@ -54,13 +63,17 @@ export const handler: Handlers = {
return new Response("Champ 'nom' manquant", { status: 400 }); return new Response("Champ 'nom' manquant", { status: 400 });
} }
const result = await db.update(ues).set({ nom }).where(eq(ues.id, idUE)).returning(); const result = await db.update(ues).set({ nom }).where(eq(ues.id, idUE))
.returning();
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(JSON.stringify(result[0]), { return new Response(JSON.stringify(result[0]), {
@@ -79,19 +92,25 @@ export const handler: Handlers = {
const idUE = parseInt(context.params.idUE); const idUE = parseInt(context.params.idUE);
if (isNaN(idUE)) { if (isNaN(idUE)) {
return new Response(JSON.stringify({ error: "Paramètre idUE invalide" }), { return new Response(
status: 400, JSON.stringify({ error: "Paramètre idUE invalide" }),
headers: { "Content-Type": "application/json" }, {
}); status: 400,
headers: { "Content-Type": "application/json" },
},
);
} }
const result = await db.delete(ues).where(eq(ues.id, idUE)).returning(); const result = await db.delete(ues).where(eq(ues.id, idUE)).returning();
if (result.length === 0) { if (result.length === 0) {
return new Response(JSON.stringify({ error: "Ressource introuvable" }), { return new Response(
status: 404, JSON.stringify({ error: "Ressource introuvable" }),
headers: { "Content-Type": "application/json" }, {
}); status: 404,
headers: { "Content-Type": "application/json" },
},
);
} }
return new Response(null, { status: 204 }); return new Response(null, { status: 204 });
+23
View File
@@ -0,0 +1,23 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "polympr-dev";
nativeBuildInputs = [
pkgs.deno
pkgs.patchelf
pkgs.tea
];
buildInputs = [
pkgs.stdenv.cc.cc.lib
];
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
# Find the dynamic linker
export NIX_LD_INTERPRETER=$(cat ${pkgs.stdenv.cc}/nix-support/dynamic-linker)
echo "Welcome to PolyMPR development shell!"
echo "Use 'deno task compile' to build the CLI."
echo "If on NixOS, it will be automatically patched."
'';
}
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
set -e
# Default output path
OUTPUT_PATH="${HOME}/.deno/bin/pmpr"
# Ensure directory exists
mkdir -p "$(dirname "$OUTPUT_PATH")"
# Check if we are on a system that needs patching (like NixOS)
IS_NIXOS=false
if [ "$(uname)" = "Linux" ]; then
if [ ! -f /lib64/ld-linux-x86-64.so.2 ] || ls -l /lib64/ld-linux-x86-64.so.2 | grep -q "stub-ld"; then
IS_NIXOS=true
fi
fi
if [ "$IS_NIXOS" = true ]; then
echo "NixOS detected. Creating a wrapper script instead of a compiled binary to avoid linking issues with Deno."
# Use absolute paths for config and script to make it work from anywhere
PROJECT_ROOT="$(pwd)"
cat > "$OUTPUT_PATH" <<EOF
#!/usr/bin/env bash
# PolyMPR CLI Wrapper for Nix
exec deno run -A --config "$PROJECT_ROOT/deno.json" "$PROJECT_ROOT/toolbox/cli.ts" "\$@"
EOF
chmod +x "$OUTPUT_PATH"
echo "Wrapper created at $OUTPUT_PATH"
else
echo "Compiling CLI to $OUTPUT_PATH..."
deno compile -A --output "$OUTPUT_PATH" toolbox/cli.ts
echo "Done."
fi