Compare commits

..

76 Commits

Author SHA1 Message Date
djalim 07b6f1e347 chore(test-framework): remove endpoint-specific tests from framework branch
Tests / Unit tests (pull_request) Successful in 12s
Tests / Integration tests (pull_request) Failing after 54s
Each endpoint's tests will be on their own issue branch (PMPR-109, etc.).
The framework branch only contains: CI config, db helpers, migrations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:55:21 +02:00
djalim c5018d9ced test(integration): add DB integration tests for students, promotions, roles, modules
Covers full CRUD for each resource via testDb:
- promotions: list, create, get by id, not found, update, delete
- students: list, filter by promo, create, get, not found, update, delete
- roles: list, create, get with permissions, update+reset perms, delete
- modules: list, create, duplicate id rejection, get, not found, update, delete

27 integration tests passing in CI (act + Gitea Actions).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:02 +02:00
djalim 367b0b2357 fix(ci): fix postgres TCP setup and truncateAll superuser error
Tests / Unit tests (pull_request) Successful in 12s
Tests / Integration tests (pull_request) Successful in 58s
- Use apt-get install + configure listen_addresses + md5 auth in pg_hba
  so psql can connect via 127.0.0.1 (not just Unix socket)
- Use pg_ctlcluster restart after config changes + wait for pg_isready
- Replace session_replication_role (requires superuser) with a single
  TRUNCATE ... CASCADE which handles FK deps without elevated privileges
- All 3 integration tests now pass in CI (act + Gitea Actions)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:22:45 +02:00
djalim e0ac451372 fix(ci): use connection URL with ssl:false in drizzle config
Tests / Unit tests (pull_request) Successful in 13s
Tests / Integration tests (pull_request) Failing after 56s
2026-04-26 00:57:38 +02:00
djalim ae5d5b64ac debug(ci): add connection diagnostics before migrate
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Failing after 56s
2026-04-26 00:54:11 +02:00
djalim 7be13737d5 fix(ci): remove unsupported --verbose from drizzle-kit migrate
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Failing after 53s
2026-04-26 00:51:22 +02:00
djalim 32052ab1c9 fix(ci): add GRANT on public schema and verbose migrate output
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Failing after 54s
2026-04-26 00:48:57 +02:00
djalim ce807391c6 fix(ci): start postgres with pg_ctlcluster instead of systemctl
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Failing after 53s
2026-04-26 00:46:02 +02:00
djalim 182342aab0 fix(ci): install postgres via apt-get instead of docker
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Failing after 25s
2026-04-26 00:43:11 +02:00
djalim d32758b310 fix(ci): use docker run instead of services for postgres 2026-04-26 00:41:20 +02:00
djalim f26b2b044f fix(ci): use bash /dev/tcp for postgres readiness check
Tests / Unit tests (pull_request) Successful in 12s
Tests / Integration tests (pull_request) Has been cancelled
2026-04-26 00:37:21 +02:00
djalim af2562ef2b fix(ci): replace pg_isready with nc for postgres readiness check
Tests / Unit tests (pull_request) Successful in 11s
Tests / Integration tests (pull_request) Has been cancelled
2026-04-26 00:35:42 +02:00
djalim f739f94403 fix(ci): use deno install for unit tests, add postgres readiness check
Tests / Unit tests (pull_request) Successful in 13s
Tests / Integration tests (pull_request) Has been cancelled
2026-04-26 00:31:15 +02:00
djalim f66de20dad fix(ci): install npm deps before running unit tests
Tests / Unit tests (pull_request) Failing after 27s
Tests / Integration tests (pull_request) Failing after 27s
2026-04-26 00:27:07 +02:00
djalim ea61d83384 fix(lint): add version to drizzle-orm imports and prefix unused NOT_FOUND
Tests / Unit tests (pull_request) Failing after 6s
Tests / Integration tests (pull_request) Failing after 1m28s
2026-04-26 00:24:27 +02:00
djalim 6402f802e9 chore(test): set up integration test framework with postgres
- Generate Drizzle migrations (databases/migrations/)
- Add databases/schema.kit.ts for drizzle-kit (Node-compatible imports)
- Update drizzle.config.ts to use schema.kit.ts
- Add deno tasks: test:unit, test:integration, migrate
- Add tests/helpers/db_integration.ts: testDb, truncateAll, seed helpers
- Add .gitea/workflows/test.yml: CI with postgres service container
- Update lint.yml: run test:unit only (no DB needed)
- Update deploy.yml: add check-code job, gate deploy on it
2026-04-26 00:23:12 +02:00
anys 66183c2ad8 feat(api): implement UE-Module coefficient update and deletion endpoint
- PUT /ue-modules/{idModule}/{idUE}/{idPromo}: update coeff for
  UE-Module-Promo association
- DELETE /ue-modules/{idModule}/{idUE}/{idPromo}: remove UE-Module-Promo
  association
- requires employee role
2026-04-23 14:01:40 +02:00
anys 9976b9e2b4 feat(api): implement UE-Module association get endpoint
- GET /ue-modules/{idModule}/{idUE}/{idPromo}: recover the detail of an
  ue-module association by its composite key
- requires employee role
2026-04-23 11:57:30 +00:00
Clément Oudelet 457b008ba3 PMPR-46/47 : PUT et DELETE /notes/{numEtud}/{idModule} 2026-04-23 11:56:20 +00:00
anys 22750ba07e feat(api): implement ajustement delete endpoint
- DELETE /ajustements/{numEtud}/{idUE}: remove ajustement from DB
- Requires employee role
- Returns 204 on success
2026-04-23 13:55:24 +02:00
anys 49876339bf feat(api): implement ajustement update endpoint
- PUT /ajustements/{numEtud}/{idUE}: update ajustement valeur
- Requires employee role
2026-04-23 11:48:31 +00:00
Clément Oudelet eeb087ea76 PMPR-36 : DELETE /ues/{idUE} - supprimer une UE 2026-04-23 13:44:43 +02:00
Clément Oudelet 7ad70c4525 GET /notes/{numEtud}/{idModule} - récupérer le détail d'une note pour un étudiant dans un module 2026-04-23 13:11:48 +02:00
Clément Oudelet 79669d60cf PMPR-38 : POST /ue-modules - associer un module à une UE 2026-04-22 20:40:28 +02:00
anys d3f1f433e1 feat(api): implement single ajustement retrieval endpoint
- GET /ajustements/{numEtud}/{idUE}: get ajustement by student numEtud
  and UE id
- Requires employee role
2026-04-22 17:24:39 +00:00
anys 022994e5a7 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
2026-04-22 17:24:07 +00:00
Clément Oudelet 33d023986c PMPR-34 : GET /ues/{idUE} - récupérer une UE par son id 2026-04-22 17:20:20 +00:00
Clément Oudelet bbc9ea58e2 PMPR-37 : GET /ue-modules - liste les associations UE-Module 2026-04-22 17:15:54 +00:00
Clément Oudelet 96b7edf77f PMPR-43 : POST /notes - créer une note 2026-04-22 17:14:45 +00:00
anys a19a1e6c13 test(api): remove enseignements unit tests
Unit tests removed as they only used mocks without real value.
2026-04-22 17:13:14 +00:00
anys 2739a01ab5 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}`
2026-04-22 17:13:14 +00:00
anys f3c1f10999 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)
2026-04-22 17:13:14 +00:00
djalim 92182b952f feat(modules): add CRUD endpoints for module resource
Implement GET, PUT, DELETE for /modules/{idModule} with 404 handling.
2026-04-22 14:47:08 +02:00
djalim cf3c7c0693 feat(admin/api): add modules endpoint with GET and POST handlers 2026-04-22 14:46:00 +02:00
djalim 5229453169 chore(drizzle.config.ts): import process for env variable support 2026-04-22 14:40:19 +02:00
djalim 6c18189d9f chore(deps): update drizzle-orm to 0.45.2 and pg to 8.20.0 2026-04-22 14:40:19 +02:00
djalim 2c1fd7e5ad feat(promotions): add CRUD endpoints for promotion by id
- GET /promotions/{idPromo} returns promotion or 404
- PUT /promotions/{idPromo} updates year or 404
- DELETE /promotions/{idPromo} deletes promotion or 404
- Only employees allowed, otherwise 403
2026-04-22 14:40:19 +02:00
Clément Oudelet 2f15efe21e PMPR-33 : POST /ues - créer une UE 2026-04-22 14:28:03 +02:00
Clément Oudelet b2847a4a7d PMPR-42 : GET /notes - récupère les notes 2026-04-22 12:20:59 +00:00
djalim 3f0c8d079f feat(students): add promotions API for employees 2026-04-22 14:13:59 +02:00
djalim 4eaea48ebd feat(students): add CRUD endpoints for student by numEtud 2026-04-22 14:11:29 +02:00
djalim f959cf0d3a feat(students): add CSV import endpoint for student data 2026-04-22 14:10:18 +02:00
djalim 0d45bd4c1c refactor(students): simplify API, remove unused imports and helpers
refactor(students): add query param filtering, enforce employee role for POST
refactor(students): return created student in POST response
2026-04-22 14:06:01 +02:00
djalim b5f134d016 feat(roles): add CRUD endpoints for role by id 2026-04-22 13:45:59 +02:00
djalim 9a3f49ecfe feat(admin/api): add roles endpoint with GET and POST 2026-04-22 13:44:30 +02:00
djalim 5a86f69093 feat: add CRUD endpoints for users by id 2026-04-22 13:42:29 +02:00
djalim 03b58e7b0a feat(admin/api/users): add GET and POST endpoints for users 2026-04-22 13:41:33 +02:00
djalim 9168ca53da feat(admin): scaffold admin module and add GET /permissions endpoint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 13:30:19 +02:00
djalim b8d359a507 feat(database): add roles, permissions, users, modules, and related tables
Add tables for role-based access control and academic entities.
Includes modules, UEs, notes, and adjustments.
Update students and mobility tables to reference new primary keys.
This enables richer data modeling for the application.
2026-04-22 13:17:08 +02:00
Clément Oudelet 32ffbb7cda PMPR-32 : GET /ues - liste toutes les UEs 2026-04-22 12:50:46 +02:00
djalim ce5acacca6 refactor(api_mock.ts): remove async from mockFetch to match signature 2026-04-21 10:14:25 +00:00
djalim 4211df32a8 style: format api mock return type and test imports/JSON body 2026-04-21 10:14:25 +00:00
djalim 50afe2ae66 test: add mock DB helper for unit tests
test: add tests for fixtures, mock fetch, mock db, and happy-dom

- Add comprehensive fixture shape tests.
- Expand mockFetch to support methods, status codes, and body tracking.
- Introduce getFetchCalls to inspect intercepted requests.
- Add mockDb helper for in-memory DB operations.
- Reorganize tests for clarity and coverage.
- Ensure happy-dom setup/cleanup works correctly.
2026-04-21 10:14:25 +00:00
djalim 17c5b33a5b refactor(test): improve fetch mock and update fixture types
Add support for HTTP methods, status codes, body and headers in the fetch
mock. Track calls and expose getFetchCalls for assertions. Update fixture
interfaces to use string IDs, add ImportResult and ApiError types, and
provide standard error constants. Adjust fixture data to match new types.
2026-04-21 10:14:25 +00:00
djalim 01fd6e9984 test: add e2e, integration, and unit tests for fixtures and mockFetch 2026-04-21 10:14:25 +00:00
djalim 332286c085 test: add API mock, fixtures, and DOM helpers for tests 2026-04-21 10:14:25 +00:00
djalim 612c41c099 ci: add test job to lint workflow and update deno.json
Add test script to deno.json
Add @std/assert, @std/testing, happy-dom dependencies
2026-04-21 10:14:25 +00:00
djalim 0f7282ba87 chore(compose.yml): update Docker Compose for production deployment
Add postgres service with environment variable for password.
Change app image to registry and adjust ports.
Update volume mount to production path.
Add deploy constraints for manager nodes.
2026-04-03 10:50:53 +02:00
djalim 9636242b42 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.
2026-04-03 10:43:29 +02:00
djalim 4949bdce5d chore(drizzle): add config for drizzle-kit migrations 2026-04-03 10:41:52 +02:00
djalim 33b8c178f2 feat(db): add PostgreSQL connection and schema definitions 2026-04-03 10:41:11 +02:00
djalim 4a2a0a3681 chore: add dependencies for dotenv, drizzle-orm, pg and dev deps
Set up environment config and database ORM
2026-04-03 10:33:38 +02:00
djalim 5932b8c2cd docs(env): add postgres env variables 2026-04-03 10:30:48 +02:00
djalim e111d5be28 Merge pull request '🐛 (Dockerfile): remove stray 'flag' argument from deno cache command' (#6) from feature/deploy into main
Build and push image / Build Docker image (push) Successful in 20s
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/6
2026-01-13 08:08:24 +00:00
djalim c70d4a5f11 🐛 (Dockerfile): remove stray 'flag' argument from deno cache command
Check Deno code / Check Deno code (pull_request) Successful in 14s
2026-01-13 09:07:01 +01:00
djalim ad5c271b05 Merge pull request 'feature/deploy : Added deploy ci tu automaticaly push image to registry' (#5) from feature/deploy into main
Build and push image / Build Docker image (push) Failing after 1m35s
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/5
2026-01-13 07:47:18 +00:00
djalim 19a588ac25 🚀 ci: add Docker build and push workflow, remove old Deno check workflow
Check Deno code / Check Deno code (pull_request) Successful in 16s
2026-01-13 08:45:23 +01:00
djalim ad524978df 🔧 (ci.yml): renamed Deno lint workflow for pull requests 2026-01-09 12:57:51 +01:00
djalim 2ce78547b7 Merge pull request '🔧 chore(ci): removed old github workflow folder' (#4) from fix/action into main
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/4
2026-01-09 11:52:25 +00:00
djalim 81a3fc0e03 🔧 chore(ci): removed old github workflow folder
Check Deno code / Check Deno code (pull_request) Successful in 13s
2026-01-09 12:50:46 +01:00
djalim 2615961bf1 Merge pull request '🔧 ci: add Deno lint and format check workflow' (#1) from feature/actions into main
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/1
2026-01-09 11:48:18 +00:00
djalim fb967e3af3 Merge pull request '🔧 chore: add env.template and load .env instead of .env.development.local' (#2) from feature/dotenv into main
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/2
2026-01-09 11:48:05 +00:00
djalim c283a34784 🔧 ci: add Deno lint and format check workflow
Check Deno code / Check Deno code (pull_request) Successful in 5s
2026-01-09 12:47:36 +01:00
djalim a48c616ecc 🔧 chore: add env.template and load .env instead of .env.development.local
Check Deno code / Check Deno code (pull_request) Successful in 5s
2026-01-09 12:47:11 +01:00
djalim cd1149a23a Merge pull request '🐛(components): add missing button types and keys' (#3) from fix/actions into main
Reviewed-on: https://git.polytech.djalim.fr/admin/PolyMPR/pulls/3
2026-01-09 11:44:17 +00:00
djalim 58c8ff56ba 🐛(components): add missing button types and keys
Check Deno code / Check Deno code (pull_request) Successful in 40s
Add type="button" to the EditMobility and UploadStudents buttons
to prevent default form submission behavior.
Include a key prop on Student components in Promotion for stable list rendering.
2026-01-09 12:41:51 +01:00
71 changed files with 4683 additions and 317 deletions
+27
View File
@@ -0,0 +1,27 @@
name: "Build and push image"
on:
push:
branches:
- main
jobs:
deploy:
name: "Build Docker image"
runs-on: ubuntu-latest
steps:
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: registry.docker.polytech.djalim.fr
username: ${{ secrets.registry_login }}
password: ${{ secrets.registry_pass }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
tags: registry.docker.polytech.djalim.fr/polympr:latest
@@ -24,3 +24,6 @@ jobs:
- name: Check linting
run: deno lint
- name: Run tests
run: deno test -A --no-check tests/
+70
View File
@@ -0,0 +1,70 @@
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: Install dependencies
run: deno install
- name: Run unit tests
run: deno task test:unit
integration:
name: "Integration tests"
runs-on: ubuntu-latest
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: Start postgres
run: |
sudo apt-get update -qq && sudo apt-get install -y -qq postgresql > /dev/null
PG_VER=$(ls /etc/postgresql/)
sudo sed -i "s/^#*listen_addresses\s*=.*/listen_addresses = '127.0.0.1'/" /etc/postgresql/$PG_VER/main/postgresql.conf
echo "host all all 127.0.0.1/32 md5" | sudo tee -a /etc/postgresql/$PG_VER/main/pg_hba.conf
sudo pg_ctlcluster $PG_VER main restart
until sudo -u postgres pg_isready -h 127.0.0.1; do sleep 1; done
sudo -u postgres psql -c "CREATE USER test WITH PASSWORD 'test';"
sudo -u postgres psql -c "CREATE DATABASE polympr_test OWNER test;"
sudo -u postgres psql -d polympr_test -c "GRANT ALL ON SCHEMA public TO test;"
- name: Apply migrations
run: |
sed 's/--> statement-breakpoint/;/g' databases/migrations/0000_square_jetstream.sql | \
PGPASSWORD=test psql -h 127.0.0.1 -U test -d polympr_test
- name: Install dependencies
run: npm install --ignore-scripts && deno install
- name: Run integration tests
env:
POSTGRES_HOST: 127.0.0.1
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`).
+1 -1
View File
@@ -3,7 +3,7 @@ FROM denoland/deno:alpine
WORKDIR /app
COPY . .
RUN deno cache main.ts --allow-import flag
RUN deno cache main.ts --allow-import
RUN deno task build
USER deno
+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: "..." }`.
+233
View File
@@ -0,0 +1,233 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"dependencies": {
"dotenv": "^17.4.0",
"drizzle-orm": "^0.45.2",
"pg": "^8.20.0",
},
"devDependencies": {
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"tsx": "^4.21.0",
},
},
},
"packages": {
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
"dotenv": ["dotenv@17.4.0", "", {}, "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ=="],
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"get-tsconfig": ["get-tsconfig@4.13.7", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q=="],
"pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="],
"pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="],
"pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="],
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
"postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
"tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
"tsx/esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
}
}
+19 -5
View File
@@ -1,10 +1,24 @@
services:
app:
container_name: deno_fresh_app
build: .
image: registry.docker.polytech.djalim.fr/polympr:latest
ports:
- "80:80"
- "443:443"
- "8008:80"
- "4430:443"
volumes:
- .:/app
- /home/kevin/PolyMPR/:/app
command: deno run -A main.ts
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
db:
image: postgres
restart: always
shm_size: 128mb
environment:
POSTGRES_PASSWORD: ${POSTGRES_PASS}
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
+14
View File
@@ -0,0 +1,14 @@
import { drizzle } from "npm:drizzle-orm@0.45.2/node-postgres";
import pg from "npm:pg@8.20.0";
const { Pool } = pg;
const pool = new Pool({
host: Deno.env.get("POSTGRES_HOST"),
port: Number(Deno.env.get("POSTGRES_PORT") ?? 5432),
user: Deno.env.get("POSTGRES_USER"),
password: Deno.env.get("POSTGRES_PASS"),
database: Deno.env.get("POSTGRES_DB"),
});
export const db = drizzle(pool);
@@ -0,0 +1,100 @@
CREATE TABLE "ajustements" (
"numEtud" integer NOT NULL,
"idUE" integer NOT NULL,
"valeur" double precision NOT NULL,
CONSTRAINT "ajustements_numEtud_idUE_pk" PRIMARY KEY("numEtud","idUE")
);
--> statement-breakpoint
CREATE TABLE "enseignements" (
"idProf" text NOT NULL,
"idModule" text NOT NULL,
"idPromo" text NOT NULL,
CONSTRAINT "enseignements_idProf_idModule_idPromo_pk" PRIMARY KEY("idProf","idModule","idPromo")
);
--> statement-breakpoint
CREATE TABLE "mobility" (
"id" serial PRIMARY KEY NOT NULL,
"studentId" integer,
"startDate" date,
"endDate" date,
"weeksCount" integer,
"destinationCountry" text,
"destinationName" text,
"mobilityStatus" text DEFAULT 'N/A'
);
--> statement-breakpoint
CREATE TABLE "modules" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "notes" (
"numEtud" integer NOT NULL,
"idModule" text NOT NULL,
"note" double precision NOT NULL,
CONSTRAINT "notes_numEtud_idModule_pk" PRIMARY KEY("numEtud","idModule")
);
--> statement-breakpoint
CREATE TABLE "permissions" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "promotions" (
"idPromo" text PRIMARY KEY NOT NULL,
"annee" text
);
--> statement-breakpoint
CREATE TABLE "role_permissions" (
"idRole" integer NOT NULL,
"idPermission" text NOT NULL,
CONSTRAINT "role_permissions_idRole_idPermission_pk" PRIMARY KEY("idRole","idPermission")
);
--> statement-breakpoint
CREATE TABLE "roles" (
"id" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "students" (
"numEtud" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL,
"prenom" text NOT NULL,
"idPromo" text
);
--> statement-breakpoint
CREATE TABLE "ue_modules" (
"idModule" text NOT NULL,
"idUE" integer NOT NULL,
"idPromo" text NOT NULL,
"coeff" double precision NOT NULL,
CONSTRAINT "ue_modules_idModule_idUE_idPromo_pk" PRIMARY KEY("idModule","idUE","idPromo")
);
--> statement-breakpoint
CREATE TABLE "ues" (
"id" serial PRIMARY KEY NOT NULL,
"nom" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" text PRIMARY KEY NOT NULL,
"nom" text NOT NULL,
"prenom" text NOT NULL,
"idRole" integer
);
--> statement-breakpoint
ALTER TABLE "ajustements" ADD CONSTRAINT "ajustements_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ajustements" ADD CONSTRAINT "ajustements_idUE_ues_id_fk" FOREIGN KEY ("idUE") REFERENCES "public"."ues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idProf_users_id_fk" FOREIGN KEY ("idProf") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "enseignements" ADD CONSTRAINT "enseignements_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "mobility" ADD CONSTRAINT "mobility_studentId_students_numEtud_fk" FOREIGN KEY ("studentId") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notes" ADD CONSTRAINT "notes_numEtud_students_numEtud_fk" FOREIGN KEY ("numEtud") REFERENCES "public"."students"("numEtud") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "notes" ADD CONSTRAINT "notes_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_idRole_roles_id_fk" FOREIGN KEY ("idRole") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_idPermission_permissions_id_fk" FOREIGN KEY ("idPermission") REFERENCES "public"."permissions"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "students" ADD CONSTRAINT "students_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idModule_modules_id_fk" FOREIGN KEY ("idModule") REFERENCES "public"."modules"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idUE_ues_id_fk" FOREIGN KEY ("idUE") REFERENCES "public"."ues"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "ue_modules" ADD CONSTRAINT "ue_modules_idPromo_promotions_idPromo_fk" FOREIGN KEY ("idPromo") REFERENCES "public"."promotions"("idPromo") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "users" ADD CONSTRAINT "users_idRole_roles_id_fk" FOREIGN KEY ("idRole") REFERENCES "public"."roles"("id") ON DELETE no action ON UPDATE no action;
@@ -0,0 +1,680 @@
{
"id": "bd317b68-1c46-4e83-b4d3-a14f68751afb",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.ajustements": {
"name": "ajustements",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idUE": {
"name": "idUE",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"valeur": {
"name": "valeur",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"ajustements_numEtud_students_numEtud_fk": {
"name": "ajustements_numEtud_students_numEtud_fk",
"tableFrom": "ajustements",
"tableTo": "students",
"columnsFrom": [
"numEtud"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ajustements_idUE_ues_id_fk": {
"name": "ajustements_idUE_ues_id_fk",
"tableFrom": "ajustements",
"tableTo": "ues",
"columnsFrom": [
"idUE"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"ajustements_numEtud_idUE_pk": {
"name": "ajustements_numEtud_idUE_pk",
"columns": [
"numEtud",
"idUE"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.enseignements": {
"name": "enseignements",
"schema": "",
"columns": {
"idProf": {
"name": "idProf",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"enseignements_idProf_users_id_fk": {
"name": "enseignements_idProf_users_id_fk",
"tableFrom": "enseignements",
"tableTo": "users",
"columnsFrom": [
"idProf"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"enseignements_idModule_modules_id_fk": {
"name": "enseignements_idModule_modules_id_fk",
"tableFrom": "enseignements",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"enseignements_idPromo_promotions_idPromo_fk": {
"name": "enseignements_idPromo_promotions_idPromo_fk",
"tableFrom": "enseignements",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"enseignements_idProf_idModule_idPromo_pk": {
"name": "enseignements_idProf_idModule_idPromo_pk",
"columns": [
"idProf",
"idModule",
"idPromo"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.mobility": {
"name": "mobility",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"studentId": {
"name": "studentId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"startDate": {
"name": "startDate",
"type": "date",
"primaryKey": false,
"notNull": false
},
"endDate": {
"name": "endDate",
"type": "date",
"primaryKey": false,
"notNull": false
},
"weeksCount": {
"name": "weeksCount",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"destinationCountry": {
"name": "destinationCountry",
"type": "text",
"primaryKey": false,
"notNull": false
},
"destinationName": {
"name": "destinationName",
"type": "text",
"primaryKey": false,
"notNull": false
},
"mobilityStatus": {
"name": "mobilityStatus",
"type": "text",
"primaryKey": false,
"notNull": false,
"default": "'N/A'"
}
},
"indexes": {},
"foreignKeys": {
"mobility_studentId_students_numEtud_fk": {
"name": "mobility_studentId_students_numEtud_fk",
"tableFrom": "mobility",
"tableTo": "students",
"columnsFrom": [
"studentId"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.modules": {
"name": "modules",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.notes": {
"name": "notes",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"note": {
"name": "note",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"notes_numEtud_students_numEtud_fk": {
"name": "notes_numEtud_students_numEtud_fk",
"tableFrom": "notes",
"tableTo": "students",
"columnsFrom": [
"numEtud"
],
"columnsTo": [
"numEtud"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"notes_idModule_modules_id_fk": {
"name": "notes_idModule_modules_id_fk",
"tableFrom": "notes",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"notes_numEtud_idModule_pk": {
"name": "notes_numEtud_idModule_pk",
"columns": [
"numEtud",
"idModule"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.permissions": {
"name": "permissions",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.promotions": {
"name": "promotions",
"schema": "",
"columns": {
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": true,
"notNull": true
},
"annee": {
"name": "annee",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.role_permissions": {
"name": "role_permissions",
"schema": "",
"columns": {
"idRole": {
"name": "idRole",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idPermission": {
"name": "idPermission",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"role_permissions_idRole_roles_id_fk": {
"name": "role_permissions_idRole_roles_id_fk",
"tableFrom": "role_permissions",
"tableTo": "roles",
"columnsFrom": [
"idRole"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"role_permissions_idPermission_permissions_id_fk": {
"name": "role_permissions_idPermission_permissions_id_fk",
"tableFrom": "role_permissions",
"tableTo": "permissions",
"columnsFrom": [
"idPermission"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"role_permissions_idRole_idPermission_pk": {
"name": "role_permissions_idRole_idPermission_pk",
"columns": [
"idRole",
"idPermission"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.roles": {
"name": "roles",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.students": {
"name": "students",
"schema": "",
"columns": {
"numEtud": {
"name": "numEtud",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"prenom": {
"name": "prenom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"students_idPromo_promotions_idPromo_fk": {
"name": "students_idPromo_promotions_idPromo_fk",
"tableFrom": "students",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ue_modules": {
"name": "ue_modules",
"schema": "",
"columns": {
"idModule": {
"name": "idModule",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idUE": {
"name": "idUE",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"idPromo": {
"name": "idPromo",
"type": "text",
"primaryKey": false,
"notNull": true
},
"coeff": {
"name": "coeff",
"type": "double precision",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"ue_modules_idModule_modules_id_fk": {
"name": "ue_modules_idModule_modules_id_fk",
"tableFrom": "ue_modules",
"tableTo": "modules",
"columnsFrom": [
"idModule"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ue_modules_idUE_ues_id_fk": {
"name": "ue_modules_idUE_ues_id_fk",
"tableFrom": "ue_modules",
"tableTo": "ues",
"columnsFrom": [
"idUE"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"ue_modules_idPromo_promotions_idPromo_fk": {
"name": "ue_modules_idPromo_promotions_idPromo_fk",
"tableFrom": "ue_modules",
"tableTo": "promotions",
"columnsFrom": [
"idPromo"
],
"columnsTo": [
"idPromo"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"ue_modules_idModule_idUE_idPromo_pk": {
"name": "ue_modules_idModule_idUE_idPromo_pk",
"columns": [
"idModule",
"idUE",
"idPromo"
]
}
},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.ues": {
"name": "ues",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true
},
"nom": {
"name": "nom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"prenom": {
"name": "prenom",
"type": "text",
"primaryKey": false,
"notNull": true
},
"idRole": {
"name": "idRole",
"type": "integer",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"users_idRole_roles_id_fk": {
"name": "users_idRole_roles_id_fk",
"tableFrom": "users",
"tableTo": "roles",
"columnsFrom": [
"idRole"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1777155028708,
"tag": "0000_square_jetstream",
"breakpoints": true
}
]
}
+99
View File
@@ -0,0 +1,99 @@
import {
date,
doublePrecision,
integer,
pgTable,
primaryKey,
serial,
text,
} from "drizzle-orm/pg-core";
export const roles = pgTable("roles", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const permissions = pgTable("permissions", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const rolePermissions = pgTable("role_permissions", {
idRole: integer("idRole").notNull().references(() => roles.id),
idPermission: text("idPermission").notNull().references(() => permissions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idRole, t.idPermission] }),
}));
export const users = pgTable("users", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idRole: integer("idRole").references(() => roles.id),
});
export const promotions = pgTable("promotions", {
id: text("idPromo").primaryKey(),
annee: text("annee"),
});
export const students = pgTable("students", {
numEtud: serial("numEtud").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idPromo: text("idPromo").references(() => promotions.id),
});
export const modules = pgTable("modules", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const enseignements = pgTable("enseignements", {
idProf: text("idProf").notNull().references(() => users.id),
idModule: text("idModule").notNull().references(() => modules.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idProf, t.idModule, t.idPromo] }),
}));
export const ues = pgTable("ues", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const ueModules = pgTable("ue_modules", {
idModule: text("idModule").notNull().references(() => modules.id),
idUE: integer("idUE").notNull().references(() => ues.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
coeff: doublePrecision("coeff").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.idModule, t.idUE, t.idPromo] }),
}));
export const notes = pgTable("notes", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idModule: text("idModule").notNull().references(() => modules.id),
note: doublePrecision("note").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idModule] }),
}));
export const ajustements = pgTable("ajustements", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idUE: integer("idUE").notNull().references(() => ues.id),
valeur: doublePrecision("valeur").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idUE] }),
}));
export const mobility = pgTable("mobility", {
id: serial("id").primaryKey(),
studentId: integer("studentId").references(() => students.numEtud),
startDate: date("startDate"),
endDate: date("endDate"),
weeksCount: integer("weeksCount"),
destinationCountry: text("destinationCountry"),
destinationName: text("destinationName"),
mobilityStatus: text("mobilityStatus").default("N/A"),
});
+99
View File
@@ -0,0 +1,99 @@
import {
date,
doublePrecision,
integer,
pgTable,
primaryKey,
serial,
text,
} from "npm:drizzle-orm@0.45.2/pg-core";
export const roles = pgTable("roles", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const permissions = pgTable("permissions", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const rolePermissions = pgTable("role_permissions", {
idRole: integer("idRole").notNull().references(() => roles.id),
idPermission: text("idPermission").notNull().references(() => permissions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idRole, t.idPermission] }),
}));
export const users = pgTable("users", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idRole: integer("idRole").references(() => roles.id),
});
export const promotions = pgTable("promotions", {
id: text("idPromo").primaryKey(),
annee: text("annee"),
});
export const students = pgTable("students", {
numEtud: serial("numEtud").primaryKey(),
nom: text("nom").notNull(),
prenom: text("prenom").notNull(),
idPromo: text("idPromo").references(() => promotions.id),
});
export const modules = pgTable("modules", {
id: text("id").primaryKey(),
nom: text("nom").notNull(),
});
export const enseignements = pgTable("enseignements", {
idProf: text("idProf").notNull().references(() => users.id),
idModule: text("idModule").notNull().references(() => modules.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
}, (t) => ({
pk: primaryKey({ columns: [t.idProf, t.idModule, t.idPromo] }),
}));
export const ues = pgTable("ues", {
id: serial("id").primaryKey(),
nom: text("nom").notNull(),
});
export const ueModules = pgTable("ue_modules", {
idModule: text("idModule").notNull().references(() => modules.id),
idUE: integer("idUE").notNull().references(() => ues.id),
idPromo: text("idPromo").notNull().references(() => promotions.id),
coeff: doublePrecision("coeff").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.idModule, t.idUE, t.idPromo] }),
}));
export const notes = pgTable("notes", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idModule: text("idModule").notNull().references(() => modules.id),
note: doublePrecision("note").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idModule] }),
}));
export const ajustements = pgTable("ajustements", {
numEtud: integer("numEtud").notNull().references(() => students.numEtud),
idUE: integer("idUE").notNull().references(() => ues.id),
valeur: doublePrecision("valeur").notNull(),
}, (t) => ({
pk: primaryKey({ columns: [t.numEtud, t.idUE] }),
}));
export const mobility = pgTable("mobility", {
id: serial("id").primaryKey(),
studentId: integer("studentId").references(() => students.numEtud),
startDate: date("startDate"),
endDate: date("endDate"),
weeksCount: integer("weeksCount"),
destinationCountry: text("destinationCountry"),
destinationName: text("destinationName"),
mobilityStatus: text("mobilityStatus").default("N/A"),
});
+1 -5
View File
@@ -3,16 +3,12 @@ import { AsyncRoute } from "$fresh/src/server/types.ts";
export interface AuthenticatedState {
isAuthenticated: true;
isFromPolytech: boolean;
role: "etudiant" | "professeur" | "administration" | "autre";
displayName: string;
uid: string;
session: CasContent;
availablePages: Record<string, string>;
}
interface UnauthenticatedState {
isAuthenticated: false;
isFromPolytech: false;
session: undefined;
}
+8 -1
View File
@@ -9,7 +9,11 @@
"start": "deno run -A --unstable-ffi --watch=static/,routes/ dev.ts",
"build": "deno run -A --unstable-ffi dev.ts build",
"preview": "deno run -A --unstable-ffi main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
"update": "deno run -A -r https://fresh.deno.dev/update .",
"test": "deno test -A --no-check tests/",
"test:unit": "deno test -A --no-check tests/unit/",
"test:integration": "deno test -A --no-check tests/integration/",
"migrate": "node_modules/.bin/drizzle-kit migrate"
},
"lint": {
"rules": {
@@ -35,6 +39,9 @@
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.2",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.1",
"$std/": "https://deno.land/std@0.216.0/",
"@std/assert": "jsr:@std/assert@^1.0.0",
"@std/testing": "jsr:@std/testing@^1.0.0",
"happy-dom": "npm:happy-dom@^16.0.0",
"$root/": "./",
"$apps/": "./routes/(apps)/"
},
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from "drizzle-kit";
import process from "node:process";
const url = process.env.DATABASE_URL ??
`postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASS}@${process.env.POSTGRES_HOST ?? "localhost"}:${process.env.POSTGRES_PORT ?? 5432}/${process.env.POSTGRES_DB}`;
export default defineConfig({
dialect: "postgresql",
schema: "./databases/schema.kit.ts",
out: "./databases/migrations",
dbCredentials: {
url,
ssl: false,
},
});
+8
View File
@@ -0,0 +1,8 @@
#Local mode, set to true to access admin pages with any users
LOCAL=false
POSTGRES_HOST = db
POSTGRES_PORT = 5432
POSTGRES_PASS = astrongpass
POSTGRES_USER = postgres
POSTGRES_DB = polympr
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
@@ -2,7 +2,7 @@ import { defineConfig } from "$fresh/server.ts";
import ensureDatabases from "$root/databases/ensure.ts";
import { load } from "@std/dotenv";
await load({ envPath: "./.env.development.local", export: true });
await load({ envPath: "./.env", export: true });
await ensureDatabases();
export default defineConfig({
server: {
-2
View File
@@ -19,7 +19,6 @@ import * as $_apps_students_partials_admin_consult from "./routes/(apps)/student
import * as $_apps_students_partials_admin_upload from "./routes/(apps)/students/partials/(admin)/upload.tsx";
import * as $_apps_students_partials_index from "./routes/(apps)/students/partials/index.tsx";
import * as $_apps_students_types_d from "./routes/(apps)/students/types.d.ts";
import * as $_403 from "./routes/_403.tsx";
import * as $_404 from "./routes/_404.tsx";
import * as $_app from "./routes/_app.tsx";
import * as $_middleware from "./routes/_middleware.ts";
@@ -65,7 +64,6 @@ const manifest = {
"./routes/(apps)/students/partials/index.tsx":
$_apps_students_partials_index,
"./routes/(apps)/students/types.d.ts": $_apps_students_types_d,
"./routes/_403.tsx": $_403,
"./routes/_404.tsx": $_404,
"./routes/_app.tsx": $_app,
"./routes/_middleware.ts": $_middleware,
+12
View File
@@ -0,0 +1,12 @@
{
"dependencies": {
"dotenv": "^17.4.0",
"drizzle-orm": "^0.45.2",
"pg": "^8.20.0"
},
"devDependencies": {
"@types/pg": "^8.20.0",
"drizzle-kit": "^0.31.10",
"tsx": "^4.21.0"
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ export const handler: MiddlewareHandler<AuthenticatedState>[] = [
context.state.availablePages = properties.pages;
if (
context.state.role == "etudiant" &&
context.state.session.eduPersonPrimaryAffiliation == "student" &&
Deno.env.get("LOCAL") != "true"
) {
properties.adminOnly.forEach((page) =>
+13
View File
@@ -0,0 +1,13 @@
import { AppProperties } from "$root/defaults/interfaces.ts";
const properties: AppProperties = {
name: "Admin",
icon: "school",
pages: {
index: "Homepage",
},
adminOnly: [],
hint: "PolyMPR module",
};
export default properties;
+70
View File
@@ -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<null, AuthenticatedState> = {
// #29 POST /enseignements
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
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" },
});
},
};
@@ -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<null, AuthenticatedState> = {
// #30 GET /enseignements/{idProf}/{idModule}/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
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<AuthenticatedState>,
): Promise<Response> {
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 });
},
};
+22
View File
@@ -0,0 +1,22 @@
import { Handlers } from "$fresh/server.ts";
export const handler: Handlers = {
async POST(request, context) {
if (request.headers.get("content-type") != "application/json") {
return new Response(null, {
status: 400,
});
}
const responseBody = {
requestBody: await request.json(),
context,
};
return new Response(JSON.stringify(responseBody), {
headers: {
"content-type": "application/json",
},
});
},
};
+63
View File
@@ -0,0 +1,63 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { modules } 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<null, AuthenticatedState> = {
// #23 GET /modules
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
const rows = await db.select().from(modules);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #24 POST /modules
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const body: { id: string; nom: string } = await request.json();
if (!body.id || !body.nom) {
return new Response(null, { status: 400 });
}
const existing = await db
.select()
.from(modules)
.where(eq(modules.id, body.id))
.then((rows) => rows[0] ?? null);
if (existing) {
return new Response(
JSON.stringify({ error: "Un module avec cet identifiant existe déjà" }),
{ status: 409, headers: { "content-type": "application/json" } },
);
}
const [created] = await db
.insert(modules)
.values({ id: body.id, nom: body.nom })
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -0,0 +1,65 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { modules } 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: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #25 GET /modules/{idModule}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const module = await db
.select()
.from(modules)
.where(eq(modules.id, context.params.idModule))
.then((rows) => rows[0] ?? null);
if (!module) return NOT_FOUND;
return new Response(JSON.stringify(module), {
headers: { "content-type": "application/json" },
});
},
// #26 PUT /modules/{idModule}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string } = await request.json();
const [updated] = await db
.update(modules)
.set({ nom: body.nom })
.where(eq(modules.id, context.params.idModule))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #27 DELETE /modules/{idModule}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const [deleted] = await db
.delete(modules)
.where(eq(modules.id, context.params.idModule))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+22
View File
@@ -0,0 +1,22 @@
import { Handlers } from "$fresh/server.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
const PERMISSIONS = [
{ id: "student_read", nom: "Consulter les élèves" },
{ id: "student_write", nom: "Gérer les élèves" },
{ id: "note_read", nom: "Consulter les notes" },
{ id: "note_write", nom: "Gérer les notes" },
{ id: "module_read", nom: "Consulter les modules" },
{ id: "module_write", nom: "Gérer les modules" },
{ id: "user_read", nom: "Consulter les utilisateurs" },
{ id: "user_write", nom: "Gérer les utilisateurs" },
{ id: "role_write", nom: "Gérer les rôles" },
] as const;
export const handler: Handlers<null, AuthenticatedState> = {
GET(_request, _context): Response {
return new Response(JSON.stringify(PERMISSIONS), {
headers: { "content-type": "application/json" },
});
},
};
+68
View File
@@ -0,0 +1,68 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { rolePermissions, roles } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
async function getRoleWithPermissions(
id: number,
): Promise<{ id: number; nom: string; permissions: string[] } | null> {
const role = await db
.select()
.from(roles)
.where(eq(roles.id, id))
.then((rows) => rows[0] ?? null);
if (!role) return null;
const perms = await db
.select({ idPermission: rolePermissions.idPermission })
.from(rolePermissions)
.where(eq(rolePermissions.idRole, id));
return {
id: role.id,
nom: role.nom,
permissions: perms.map((p) => p.idPermission),
};
}
export const handler: Handlers<null, AuthenticatedState> = {
// #65 GET /roles
async GET(
_request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const allRoles = await db.select().from(roles);
const result = await Promise.all(
allRoles.map((r) => getRoleWithPermissions(r.id)),
);
return new Response(JSON.stringify(result), {
headers: { "content-type": "application/json" },
});
},
// #66 POST /roles
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string } = await request.json();
if (!body.nom) {
return new Response(null, { status: 400 });
}
const [created] = await db
.insert(roles)
.values({ nom: body.nom })
.returning();
return new Response(
JSON.stringify({ id: created.id, nom: created.nom, permissions: [] }),
{ status: 201, headers: { "content-type": "application/json" } },
);
},
};
+101
View File
@@ -0,0 +1,101 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { rolePermissions, roles } 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: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
async function getRoleWithPermissions(
id: number,
): Promise<{ id: number; nom: string; permissions: string[] } | null> {
const role = await db
.select()
.from(roles)
.where(eq(roles.id, id))
.then((rows) => rows[0] ?? null);
if (!role) return null;
const perms = await db
.select({ idPermission: rolePermissions.idPermission })
.from(rolePermissions)
.where(eq(rolePermissions.idRole, id));
return {
id: role.id,
nom: role.nom,
permissions: perms.map((p) => p.idPermission),
};
}
export const handler: Handlers<null, AuthenticatedState> = {
// #67 GET /roles/{idRole}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
const role = await getRoleWithPermissions(id);
if (!role) return NOT_FOUND;
return new Response(JSON.stringify(role), {
headers: { "content-type": "application/json" },
});
},
// #68 PUT /roles/{idRole}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
const body: { nom: string; permissions: string[] } = await request.json();
const [updated] = await db
.update(roles)
.set({ nom: body.nom })
.where(eq(roles.id, id))
.returning();
if (!updated) return NOT_FOUND;
// Reset permissions
await db.delete(rolePermissions).where(eq(rolePermissions.idRole, id));
if (body.permissions?.length) {
await db.insert(rolePermissions).values(
body.permissions.map((p) => ({ idRole: id, idPermission: p })),
);
}
const role = await getRoleWithPermissions(id);
return new Response(JSON.stringify(role), {
headers: { "content-type": "application/json" },
});
},
// #69 DELETE /roles/{idRole}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const id = Number(context.params.idRole);
// Cascade delete role_permissions first
await db.delete(rolePermissions).where(eq(rolePermissions.idRole, id));
const [deleted] = await db
.delete(roles)
.where(eq(roles.id, id))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+67
View File
@@ -0,0 +1,67 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { users } 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<null, AuthenticatedState> = {
// #60 GET /users
async GET(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const url = new URL(request.url);
const idRole = url.searchParams.get("idRole");
const rows = idRole
? await db.select().from(users).where(eq(users.idRole, Number(idRole)))
: await db.select().from(users);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #61 POST /users
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { id: string; nom: string; prenom: string; idRole: number } =
await request.json();
if (!body.id || !body.nom || !body.prenom) {
return new Response(null, { status: 400 });
}
const existing = await db
.select()
.from(users)
.where(eq(users.id, body.id))
.then((rows) => rows[0] ?? null);
if (existing) {
return new Response(
JSON.stringify({
error: "Un utilisateur avec cet identifiant existe déjà",
}),
{ status: 409, headers: { "content-type": "application/json" } },
);
}
const [created] = await db
.insert(users)
.values({
id: body.id,
nom: body.nom,
prenom: body.prenom,
idRole: body.idRole,
})
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
+66
View File
@@ -0,0 +1,66 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { users } 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: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #62 GET /users/{id}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const user = await db
.select()
.from(users)
.where(eq(users.id, context.params.id))
.then((rows) => rows[0] ?? null);
if (!user) return NOT_FOUND;
return new Response(JSON.stringify(user), {
headers: { "content-type": "application/json" },
});
},
// #63 PUT /users/{id}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const body: { nom: string; prenom: string; idRole: number } = await request
.json();
const [updated] = await db
.update(users)
.set({ nom: body.nom, prenom: body.prenom, idRole: body.idRole })
.where(eq(users.id, context.params.id))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #64 DELETE /users/{id}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const [deleted] = await db
.delete(users)
.where(eq(users.id, context.params.id))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+2
View File
@@ -0,0 +1,2 @@
import makeIndex from "$root/defaults/makeIndex.ts";
export default makeIndex(import.meta.dirname!);
+13
View File
@@ -0,0 +1,13 @@
import {
getPartialsConfig,
makePartials,
} from "$root/defaults/makePartials.tsx";
import { FreshContext } from "$fresh/server.ts";
import { State } from "$root/routes/_middleware.ts";
export function Index(_request: Request, _context: FreshContext<State>) {
return <h2>Welcome to Admin.</h2>;
}
export const config = getPartialsConfig();
export default makePartials(Index);
@@ -240,7 +240,7 @@ export default function EditMobility() {
</table>
</div>
))}
<button onClick={handleSave} disabled={isSaving}>
<button type="button" onClick={handleSave} disabled={isSaving}>
{isSaving ? "Saving..." : "Confirm"}
</button>
</section>
+64 -110
View File
@@ -1,55 +1,40 @@
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@0.45.2";
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 +43,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 +50,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 +61,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,40 +79,38 @@ 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:", {
id,
studentId,
startDate,
endDate,
calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
});
insertQuery.run(
id,
studentId,
startDate,
endDate,
calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
);
await db
.insert(mobility)
.values({
id,
studentId,
startDate,
endDate,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
})
.onConflictDoUpdate({
target: mobility.id,
set: {
startDate,
endDate,
weeksCount: calculatedWeeksCount,
destinationCountry,
destinationName,
mobilityStatus,
},
});
}
connection.close();
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", {
status: 200,
});
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
}
export const config = getPartialsConfig();
+83
View File
@@ -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<null, AuthenticatedState> = {
// #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<AuthenticatedState>,
): Promise<Response> {
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 });
}
},
};
@@ -0,0 +1,107 @@
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<null, AuthenticatedState> = {
// #50 GET /ajustements/{numEtud}/{idUE}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
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" },
});
},
// #51 PUT /ajustements/{numEtud}/{idUE}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
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 body: { valeur: number } = await request.json();
if (body.valeur === undefined) {
return new Response(JSON.stringify({ error: "Champ requis: valeur" }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
const [updated] = await db
.update(ajustements)
.set({ valeur: body.valeur })
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #52 DELETE /ajustements/{numEtud}/{idUE}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
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 [deleted] = await db
.delete(ajustements)
.where(eq(ajustements.numEtud, numEtud), eq(ajustements.idUE, idUE))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+64
View File
@@ -0,0 +1,64 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts";
import { notes } from "../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// #42 GET /notes
async GET(request) {
try {
const url = new URL(request.url);
const numEtudParam = url.searchParams.get("numEtud");
const idModule = url.searchParams.get("idModule");
let query = db.select().from(notes).$dynamic();
if (numEtudParam) {
const numEtud = parseInt(numEtudParam);
if (isNaN(numEtud)) {
return new Response("Paramètre numEtud invalide", { status: 400 });
}
query = query.where(eq(notes.numEtud, numEtud));
}
if (idModule) {
query = query.where(eq(notes.idModule, idModule));
}
const result = await query;
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching notes:", error);
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 });
}
},
};
@@ -0,0 +1,139 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../../databases/db.ts";
import { notes } from "../../../../../../databases/schema.ts";
import { and, eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = {
// #45 GET /notes/:numEtud/:idModule
async GET(_request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.select().from(notes).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
);
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 note:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #46 PUT /notes/:numEtud/:idModule
async PUT(request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const body = await request.json();
const { note } = body;
if (note === undefined) {
return new Response("Champ 'note' manquant", { status: 400 });
}
const result = await db.update(notes).set({ note }).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
).returning();
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 updating note:", error);
return new Response("Failed to update note", { status: 500 });
}
},
// #47 DELETE /notes/:numEtud/:idModule
async DELETE(_request, context) {
try {
const numEtud = parseInt(context.params.numEtud);
const { idModule } = context.params;
if (isNaN(numEtud)) {
return new Response(
JSON.stringify({ error: "Paramètre numEtud invalide" }),
{
status: 400,
headers: { "Content-Type": "application/json" },
},
);
}
const result = await db.delete(notes).where(
and(
eq(notes.numEtud, numEtud),
eq(notes.idModule, idModule),
),
).returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(null, { status: 204 });
} catch (error) {
console.error("Error deleting note:", error);
return new Response("Failed to delete note", { status: 500 });
}
},
};
+66
View File
@@ -0,0 +1,66 @@
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@0.45.2";
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 });
}
},
// #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 });
}
},
};
@@ -0,0 +1,139 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { ueModules } 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: "Association UE-Module introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
const BAD_REQUEST = new Response(
JSON.stringify({ error: "Paramètres invalides" }),
{ status: 400, headers: { "content-type": "application/json" } },
);
export const handler: Handlers<null, AuthenticatedState> = {
// #39 GET /ue-modules/{idModule}/{idUE}/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const ueModuleAssociation = await db
.select()
.from(ueModules)
.where(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
)
.then((rows) => rows[0] ?? null);
if (!ueModuleAssociation) return NOT_FOUND;
return new Response(JSON.stringify(ueModuleAssociation), {
headers: { "content-type": "application/json" },
});
},
// #40 PUT /ue-modules/{idModule}/{idUE}/{idPromo}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const body: { coeff: number } = await request.json();
if (typeof body.coeff !== "number") {
return new Response(
JSON.stringify({ error: "Le champ 'coeff' doit être un nombre" }),
{ status: 400, headers: { "content-type": "application/json" } },
);
}
const [updated] = await db
.update(ueModules)
.set({ coeff: body.coeff })
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
.returning();
if (!updated) return NOT_FOUND;
return new Response(
JSON.stringify({
idModule: updated.idModule,
idUE: updated.idUE,
idPromo: updated.idPromo,
coeff: updated.coeff,
}),
{
headers: { "content-type": "application/json" },
},
);
},
// #41 DELETE /ue-modules/{idModule}/{idUE}/{idPromo}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const idModule = context.params.idModule;
const idUE = Number(context.params.idUE);
const idPromo = context.params.idPromo;
if (isNaN(idUE)) {
return BAD_REQUEST;
}
const [deleted] = await db
.delete(ueModules)
.where(
and(
eq(ueModules.idModule, idModule),
eq(ueModules.idUE, idUE),
eq(ueModules.idPromo, idPromo),
),
)
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+42
View File
@@ -0,0 +1,42 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../databases/db.ts";
import { ues } from "../../../../databases/schema.ts";
export const handler: Handlers = {
// #32 GET /ues
async GET() {
try {
const result = await db.select().from(ues);
return new Response(JSON.stringify(result), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error fetching UEs:", error);
return new Response("Failed to fetch data", { status: 500 });
}
},
// #33 POST /ues
async POST(request) {
try {
const body = await request.json();
const { nom } = body;
if (!nom) {
return new Response("Champ 'nom' manquant", { status: 400 });
}
const result = await db.insert(ues).values({ nom }).returning();
return new Response(JSON.stringify(result[0]), {
status: 201,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error creating UE:", error);
return new Response("Failed to create UE", { status: 500 });
}
},
};
+122
View File
@@ -0,0 +1,122 @@
import { Handlers } from "$fresh/server.ts";
import { db } from "../../../../../databases/db.ts";
import { ues } from "../../../../../databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
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 });
}
},
// #35 PUT /ues/:idUE
async PUT(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 body = await request.json();
const { nom } = body;
if (!nom) {
return new Response("Champ 'nom' manquant", { status: 400 });
}
const result = await db.update(ues).set({ nom }).where(eq(ues.id, idUE))
.returning();
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 updating UE:", error);
return new Response("Failed to update UE", { status: 500 });
}
},
// #36 DELETE /ues/:idUE
async DELETE(_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.delete(ues).where(eq(ues.id, idUE)).returning();
if (result.length === 0) {
return new Response(
JSON.stringify({ error: "Ressource introuvable" }),
{
status: 404,
headers: { "Content-Type": "application/json" },
},
);
}
return new Response(null, { status: 204 });
} catch (error) {
console.error("Error deleting UE:", error);
return new Response("Failed to delete UE", { status: 500 });
}
},
};
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await
async function Courses(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
}
export const config = getPartialsConfig();
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
}
export const config = getPartialsConfig();
+1 -1
View File
@@ -7,7 +7,7 @@ import { State } from "$root/routes/_middleware.ts";
// deno-lint-ignore require-await
async function Notes(_request: Request, context: FreshContext<State>) {
return <h2>Welcome to {context.state.displayName || 'Guest'}.</h2>;
return <h2>Welcome to {context.state.session?.displayName}.</h2>;
}
export const config = getPartialsConfig();
@@ -22,7 +22,7 @@ export default function Promotion(props: PromotionProps) {
<tbody>
{props.students
.filter((student) => student.promotionId === props.promo.id)
.map((student) => <Student student={student} />)}
.map((student) => <Student key={student.id} student={student} />)}
</tbody>
</table>
</div>
@@ -104,7 +104,7 @@ export default function UploadStudents() {
return (
<>
<input type="file" accept=".xlsx, .xls" onChange={handleFileChange} />
<button onClick={confirmUpload}>Confirm Upload</button>
<button type="button" onClick={confirmUpload}>Confirm Upload</button>
<p>{statusMessage.value}</p>
</>
);
+49
View File
@@ -0,0 +1,49 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { promotions } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
export const handler: Handlers<null, AuthenticatedState> = {
// #13 GET /promotions
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
const rows = await db.select().from(promotions);
return new Response(JSON.stringify(rows), {
headers: { "content-type": "application/json" },
});
},
// #14 POST /promotions
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const body: { idPromo: string; annee: string } = await request.json();
if (!body.idPromo || !body.annee) {
return new Response(null, { status: 400 });
}
const [created] = await db
.insert(promotions)
.values({ id: body.idPromo, annee: body.annee })
.returning();
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -0,0 +1,79 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { promotions } 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: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #15 GET /promotions/{idPromo}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const promo = await db
.select()
.from(promotions)
.where(eq(promotions.id, context.params.idPromo))
.then((rows) => rows[0] ?? null);
if (!promo) return NOT_FOUND;
return new Response(JSON.stringify(promo), {
headers: { "content-type": "application/json" },
});
},
// #16 PUT /promotions/{idPromo}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const body: { annee: string } = await request.json();
const [updated] = await db
.update(promotions)
.set({ annee: body.annee })
.where(eq(promotions.id, context.params.idPromo))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #17 DELETE /promotions/{idPromo}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const [deleted] = await db
.delete(promotions)
.where(eq(promotions.id, context.params.idPromo))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
+41 -131
View File
@@ -1,151 +1,61 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import connect from "$root/databases/connect.ts";
import { db } from "$root/databases/db.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { Database } from "@db/sqlite";
/**
* 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,
): { student: Student | null; promo: Promotion | null } {
const studentQuery = "select * from students where userId = ?";
const student: Student | undefined = database.prepare(studentQuery).get(
userId,
);
if (!student) {
return { student: null, promo: null };
}
const promoQuery = "select * from promotions where id = ?";
const promo: Promotion | undefined = database.prepare(promoQuery).get(
student.promotionId,
);
return { student, promo: promo ?? null };
}
/**
* 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();
const promosQuery = "select * from promotions where promotions.current < 6";
const promos: Promotion[] | undefined = database.prepare(promosQuery).all();
return { students, 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,
);
}
}
import { eq } from "npm:drizzle-orm@0.45.2";
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
// #7 GET /students
async GET(
_request: Request,
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
using connection = connect("students");
const database = connection.database;
if (context.state.role == "etudiant") {
return new Response(
JSON.stringify(getItself(database, context.state.uid)),
{
headers: {
"content-type": "application/json",
},
},
);
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(JSON.stringify([]), {
headers: { "content-type": "application/json" },
});
}
return new Response(
JSON.stringify(getAll(database)),
{
headers: {
"content-type": "application/json",
},
},
);
const url = new URL(request.url);
const idPromo = url.searchParams.get("idPromo");
const rows = idPromo
? await db.select().from(students).where(eq(students.idPromo, idPromo))
: await db.select().from(students);
return new Response(JSON.stringify(rows), {
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.
*/
// #8 POST /students
async POST(
request: Request,
_context: FreshContext<AuthenticatedState>,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
const { students, promo }: { students: Student[]; promo: string } =
await request.json();
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
const body: {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
} = await request.json();
if (!body.nom || !body.prenom || !body.idPromo) {
return new Response(null, { status: 400 });
}
using connection = connect("students");
const database = connection.database;
const [created] = await db
.insert(students)
.values({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.returning();
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);
const { id: promoId }: { id: string } = database
.prepare("select id from promotions where endyear = ? and current = ?")
.get(endyear, current)!;
addStudents(database, students, promoId);
return new Response(null, { status: 201 });
return new Response(JSON.stringify(created), {
status: 201,
headers: { "content-type": "application/json" },
});
},
};
@@ -0,0 +1,83 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { students } 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: "Ressource introuvable" }),
{ status: 404, headers: { "content-type": "application/json" } },
);
const FORBIDDEN = new Response(null, { status: 403 });
export const handler: Handlers<null, AuthenticatedState> = {
// #10 GET /students/{numEtud}
async GET(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const student = await db
.select()
.from(students)
.where(eq(students.numEtud, numEtud))
.then((rows) => rows[0] ?? null);
if (!student) return NOT_FOUND;
return new Response(JSON.stringify(student), {
headers: { "content-type": "application/json" },
});
},
// #11 PUT /students/{numEtud}
async PUT(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const body: { nom: string; prenom: string; idPromo: string } = await request
.json();
const [updated] = await db
.update(students)
.set({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.where(eq(students.numEtud, numEtud))
.returning();
if (!updated) return NOT_FOUND;
return new Response(JSON.stringify(updated), {
headers: { "content-type": "application/json" },
});
},
// #12 DELETE /students/{numEtud}
async DELETE(
_request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return FORBIDDEN;
}
const numEtud = Number(context.params.numEtud);
const [deleted] = await db
.delete(students)
.where(eq(students.numEtud, numEtud))
.returning();
if (!deleted) return NOT_FOUND;
return new Response(null, { status: 204 });
},
};
@@ -0,0 +1,64 @@
import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
// #9 POST /students/import-csv
export const handler: Handlers<null, AuthenticatedState> = {
async POST(
request: Request,
context: FreshContext<AuthenticatedState>,
): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") {
return new Response(null, { status: 403 });
}
const formData = await request.formData();
const file = formData.get("file") as File | null;
const idPromo = formData.get("idPromo") as string | null;
if (!file || !idPromo) {
return new Response(null, { status: 400 });
}
const text = await file.text();
const lines = text.trim().split("\n");
let imported = 0;
const errors: { line: number; message: string }[] = [];
for (let i = 0; i < lines.length; i++) {
const lineNum = i + 1;
const cols = lines[i].split(",").map((c) => c.trim());
const [numEtudStr, nom, prenom] = cols;
if (!numEtudStr) {
errors.push({ line: lineNum, message: "Numéro étudiant manquant" });
continue;
}
const numEtud = Number(numEtudStr);
if (isNaN(numEtud)) {
errors.push({ line: lineNum, message: "Numéro étudiant invalide" });
continue;
}
if (!nom || !prenom) {
errors.push({ line: lineNum, message: "Nom ou prénom manquant" });
continue;
}
await db
.insert(students)
.values({ nom, prenom, idPromo })
.onConflictDoNothing();
imported++;
}
return new Response(JSON.stringify({ imported, errors }), {
headers: { "content-type": "application/json" },
});
},
};
+7 -1
View File
@@ -8,7 +8,13 @@ import SelfPortrait from "$root/routes/(apps)/students/(_components)/SelfPortrai
// deno-lint-ignore require-await
export async function Index(_request: Request, context: FreshContext<State>) {
return <h2>Welcome {context.state.displayName || 'Guest'}!</h2>;
return (
<>
<h2>Welcome {context.state.session?.givenName}!</h2>
<h3>Your amU identity</h3>
<SelfPortrait self={context.state.session!} />
</>
);
}
export const config = getPartialsConfig();
-12
View File
@@ -1,12 +0,0 @@
import { Head } from "$fresh/runtime.ts";
export default function Error403() {
return (
<>
<Head>
<title>403 - Forbidden</title>
</Head>
<p>403</p>
</>
);
}
+8 -39
View File
@@ -44,7 +44,6 @@ export function getKey(user: string): string {
export const handler: MiddlewareHandler<State>[] = [
/**
* Check if user is authenticated and add session to context accordingly.
* Only authenticated users who are members of Polytech are allowed.
* @param request The HTTP incomming request.
* @param context The Fresh context object with custom `State`.
* @returns The response from the next middleware.
@@ -56,7 +55,6 @@ export const handler: MiddlewareHandler<State>[] = [
const cookies = getCookies(request.headers);
if (!cookies["sessionToken"]) {
context.state.isAuthenticated = false;
context.state.isFromPolytech = false;
return await context.next();
}
@@ -69,27 +67,9 @@ export const handler: MiddlewareHandler<State>[] = [
);
if (context.state.isAuthenticated) {
const session: CasContent =
(getJwtPayload(cookies["sessionToken"]) as LoginJWT).user;
const isFromPolytech = session.amuComposante.includes("polytech");
context.state.isFromPolytech = isFromPolytech;
if (isFromPolytech) {
context.state.displayName = session.displayName;
context.state.uid = session.uid;
if (session.eduPersonPrimaryAffiliation == "faculty") {
context.state.role = "professeur"
} else if (session.eduPersonPrimaryAffiliation == "employee") {
context.state.role = "administration"
} else if (session.eduPersonPrimaryAffiliation == "student") {
context.state.role = "etudiant";
} else {
context.state.role = "autre";
}
}
context.state.session = session;
}
return await context.next();
@@ -107,24 +87,13 @@ export const handler: MiddlewareHandler<State>[] = [
): Promise<Response> {
const url = new URL(request.url);
if (!isRoutePublic(url.pathname)) {
if (!context.state.isAuthenticated) {
return new Response(null, {
status: 302,
headers: {
Location: "/login",
},
});
}
if (!context.state.isFromPolytech) {
return new Response(null, {
status: 403,
headers: {
Location: "/403",
},
});
}
if (!isRoutePublic(url.pathname) && !context.state.isAuthenticated) {
return new Response(null, {
status: 302,
headers: {
Location: "/login",
},
});
}
return await context.next();
+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."
'';
}
View File
+123
View File
@@ -0,0 +1,123 @@
// Mock de fetch() pour les tests — supporte méthodes HTTP et status codes
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
export interface MockRoute {
method?: HttpMethod;
status?: number;
body?: unknown;
headers?: Record<string, string>;
}
// deno-lint-ignore no-explicit-any
let _originalFetch: ((input: any, init?: any) => Promise<Response>) | null =
null;
let _calls: { url: string; method: string; body?: unknown }[] = [];
/**
* Remplace globalThis.fetch par un mock configurable.
*
* Usage simple (GET 200 par défaut) :
* mockFetch({ "/students": studentsData })
*
* Usage avancé (méthode + status) :
* mockFetch({ "/students": { method: "POST", status: 201, body: newStudent } })
*/
export function mockFetch(
routes: Record<string, unknown | MockRoute>,
): void {
_originalFetch = globalThis.fetch;
_calls = [];
globalThis.fetch = (
input: string | URL | Request,
init?: RequestInit,
): Promise<Response> => {
const url = typeof input === "string"
? input
: input instanceof URL
? input.toString()
: input.url;
const method = (init?.method ?? "GET").toUpperCase();
// Parse le body si présent
let reqBody: unknown = undefined;
if (init?.body) {
try {
reqBody = JSON.parse(init.body as string);
} catch {
reqBody = init.body;
}
}
_calls.push({ url, method, body: reqBody });
for (const [pattern, config] of Object.entries(routes)) {
if (!url.includes(pattern)) continue;
// Config simple : la valeur est directement le body de réponse (GET 200)
if (!isRouteConfig(config)) {
return new Response(JSON.stringify(config), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
// Config avancée : vérifier la méthode si spécifiée
if (config.method && config.method !== method) continue;
const status = config.status ?? 200;
// 204 : pas de body
if (status === 204) {
return new Response(null, { status: 204 });
}
return new Response(
config.body !== undefined ? JSON.stringify(config.body) : null,
{
status,
headers: {
"Content-Type": "application/json",
...config.headers,
},
},
);
}
return new Response(JSON.stringify({ error: "Not Found" }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
};
}
/**
* Restaure le fetch original.
*/
export function restoreFetch(): void {
if (_originalFetch) {
globalThis.fetch = _originalFetch;
_originalFetch = null;
}
_calls = [];
}
/**
* Retourne la liste des appels fetch interceptés.
*/
export function getFetchCalls(): {
url: string;
method: string;
body?: unknown;
}[] {
return [..._calls];
}
function isRouteConfig(value: unknown): value is MockRoute {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return false;
}
const v = value as Record<string, unknown>;
return "status" in v || "method" in v || "body" in v;
}
+89
View File
@@ -0,0 +1,89 @@
// Helper pour les tests d'intégration avec PostgreSQL
// Nécessite les variables d'environnement POSTGRES_* (ou TEST_DATABASE_URL)
import { drizzle } from "npm:drizzle-orm@0.45.2/node-postgres";
import pg from "npm:pg@8.20.0";
import * as schema from "$root/databases/schema.ts";
const { Pool } = pg;
function createTestPool(): pg.Pool {
const url = Deno.env.get("TEST_DATABASE_URL");
if (url) {
return new Pool({ connectionString: url });
}
return new Pool({
host: Deno.env.get("POSTGRES_HOST") ?? "localhost",
port: Number(Deno.env.get("POSTGRES_PORT") ?? 5432),
user: Deno.env.get("POSTGRES_USER") ?? "test",
password: Deno.env.get("POSTGRES_PASS") ?? "test",
database: Deno.env.get("POSTGRES_DB") ?? "polympr_test",
ssl: false,
});
}
export const testPool = createTestPool();
export const testDb = drizzle(testPool, { schema });
const ALL_TABLES =
'"mobility","ajustements","notes","ue_modules","enseignements","role_permissions","students","users","modules","ues","promotions","permissions","roles"';
/**
* Vide toutes les tables dans le bon ordre.
* À appeler dans beforeEach de chaque test d'intégration.
*/
export async function truncateAll(): Promise<void> {
const client = await testPool.connect();
try {
await client.query(
`TRUNCATE TABLE ${ALL_TABLES} RESTART IDENTITY CASCADE`,
);
} finally {
client.release();
}
}
/**
* Ferme le pool à la fin de la suite de tests.
*/
export async function closeTestPool(): Promise<void> {
await testPool.end();
}
// --- Helpers d'insertion de fixtures ---
export async function seedRoles(
rows: { nom: string }[],
): Promise<typeof schema.roles.$inferSelect[]> {
return await testDb.insert(schema.roles).values(rows).returning();
}
export async function seedPromotions(
rows: { id: string; annee?: string }[],
): Promise<typeof schema.promotions.$inferSelect[]> {
return await testDb.insert(schema.promotions).values(rows).returning();
}
export async function seedStudents(
rows: { nom: string; prenom: string; idPromo?: string }[],
): Promise<typeof schema.students.$inferSelect[]> {
return await testDb.insert(schema.students).values(rows).returning();
}
export async function seedModules(
rows: { id: string; nom: string }[],
): Promise<typeof schema.modules.$inferSelect[]> {
return await testDb.insert(schema.modules).values(rows).returning();
}
export async function seedUes(
rows: { nom: string }[],
): Promise<typeof schema.ues.$inferSelect[]> {
return await testDb.insert(schema.ues).values(rows).returning();
}
export async function seedUsers(
rows: { id: string; nom: string; prenom: string; idRole?: number }[],
): Promise<typeof schema.users.$inferSelect[]> {
return await testDb.insert(schema.users).values(rows).returning();
}
+122
View File
@@ -0,0 +1,122 @@
// Mock de la couche Drizzle pour les tests unitaires/intégration
// Permet de tester les handlers sans connexion PostgreSQL
export interface MockQueryResult<T> {
rows: T[];
}
export interface MockDbConfig {
// Table name → array of rows
// deno-lint-ignore no-explicit-any
tables: Record<string, Record<string, any>[]>;
}
/**
* Crée un mock de la DB Drizzle.
* Simule select/insert/update/delete avec un store en mémoire.
*
* Usage :
* ```ts
* const db = createMockDb({
* tables: {
* students: [{ numEtud: 21212006, nom: "Dupont", ... }],
* notes: [],
* }
* });
*
* // Lire toutes les lignes d'une table
* const rows = db.getTable("students");
*
* // Insérer
* db.insert("students", { numEtud: 21212009, nom: "Test", ... });
*
* // Trouver par clé
* const student = db.findOne("students", (r) => r.numEtud === 21212006);
*
* // Supprimer
* db.deleteWhere("students", (r) => r.numEtud === 21212006);
* ```
*/
export function createMockDb(config: MockDbConfig) {
// Deep clone pour éviter les mutations entre tests
// deno-lint-ignore no-explicit-any
const tables: Record<string, Record<string, any>[]> = {};
for (const [name, rows] of Object.entries(config.tables)) {
tables[name] = rows.map((r) => ({ ...r }));
}
return {
/** Retourne toutes les lignes d'une table */
getTable<T = Record<string, unknown>>(name: string): T[] {
return (tables[name] ?? []) as T[];
},
/** Retourne les lignes qui matchent le filtre */
findMany<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): T[] {
return (this.getTable<T>(name)).filter(predicate);
},
/** Retourne la première ligne qui matche, ou undefined */
findOne<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): T | undefined {
return (this.getTable<T>(name)).find(predicate);
},
/** Insère une ligne dans la table */
insert<T = Record<string, unknown>>(name: string, row: T): T {
if (!tables[name]) tables[name] = [];
const copy = { ...row } as T;
// deno-lint-ignore no-explicit-any
tables[name].push(copy as any);
return copy;
},
/** Met à jour les lignes qui matchent le prédicat */
updateWhere<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
updates: Partial<T>,
): number {
const rows = this.getTable<T>(name);
let count = 0;
for (const row of rows) {
if (predicate(row)) {
Object.assign(row as Record<string, unknown>, updates);
count++;
}
}
return count;
},
/** Supprime les lignes qui matchent le prédicat */
deleteWhere<T = Record<string, unknown>>(
name: string,
predicate: (row: T) => boolean,
): number {
const before = (tables[name] ?? []).length;
tables[name] = (tables[name] ?? []).filter(
(r) => !predicate(r as unknown as T),
);
return before - tables[name].length;
},
/** Vide une table */
clear(name: string): void {
tables[name] = [];
},
/** Vide toutes les tables */
reset(): void {
for (const name of Object.keys(tables)) {
tables[name] = [];
}
},
};
}
export type MockDb = ReturnType<typeof createMockDb>;
+137
View File
@@ -0,0 +1,137 @@
// Types et données de test alignés sur l'API REST PolyMPR
// --- Types ---
export interface Student {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
}
export interface Promotion {
idPromo: string;
annee: string;
}
export interface Prof {
id: number;
nom: string;
prenom: string;
}
export interface Module {
id: string;
nom: string;
}
export interface Note {
note: number;
numEtud: number;
idModule: string;
}
export interface UE {
id: number;
nom: string;
}
export interface UeModule {
idModule: string;
idUE: number;
idPromo: string;
coeff: number;
}
export interface Enseignement {
idProf: number;
idModule: string;
idPromo: string;
}
export interface Ajustement {
numEtud: number;
idUE: number;
valeur: number;
}
export interface ImportResult {
imported: number;
errors: { line: number; message: string }[];
}
export interface ApiError {
error: string;
}
// --- Fixtures ---
export const students: Student[] = [
{ numEtud: 21212006, nom: "Dupont", prenom: "Jean", idPromo: "4AFISE25/26" },
{
numEtud: 21212007,
nom: "Martin",
prenom: "Alice",
idPromo: "4AFISE25/26",
},
{
numEtud: 21212008,
nom: "Durand",
prenom: "Claire",
idPromo: "3AFISE25/26",
},
];
export const promotions: Promotion[] = [
{ idPromo: "4AFISE25/26", annee: "2025" },
{ idPromo: "3AFISE25/26", annee: "2025" },
{ idPromo: "JIA4A2526", annee: "2025" },
];
export const profs: Prof[] = [
{ id: 1, nom: "Leclerc", prenom: "Jean" },
{ id: 2, nom: "Moreau", prenom: "Sophie" },
];
export const modules: Module[] = [
{ id: "JIN702C", nom: "Optimisation" },
{ id: "JIN703C", nom: "Informatique" },
{ id: "JIN704C", nom: "Physique" },
];
export const notes: Note[] = [
{ note: 15.5, numEtud: 21212006, idModule: "JIN702C" },
{ note: 12.0, numEtud: 21212006, idModule: "JIN703C" },
{ note: 18.0, numEtud: 21212007, idModule: "JIN702C" },
{ note: 9.0, numEtud: 21212008, idModule: "JIN704C" },
];
export const ues: UE[] = [
{ id: 1, nom: "UE Informatique" },
{ id: 2, nom: "UE Mathématiques" },
];
export const ueModules: UeModule[] = [
{ idModule: "JIN702C", idUE: 1, idPromo: "4AFISE25/26", coeff: 3.0 },
{ idModule: "JIN703C", idUE: 2, idPromo: "4AFISE25/26", coeff: 4.0 },
{ idModule: "JIN704C", idUE: 1, idPromo: "3AFISE25/26", coeff: 2.0 },
];
export const enseignements: Enseignement[] = [
{ idProf: 1, idModule: "JIN702C", idPromo: "4AFISE25/26" },
{ idProf: 2, idModule: "JIN703C", idPromo: "4AFISE25/26" },
{ idProf: 1, idModule: "JIN704C", idPromo: "3AFISE25/26" },
];
export const ajustements: Ajustement[] = [
{ numEtud: 21212006, idUE: 1, valeur: 13.25 },
{ numEtud: 21212008, idUE: 1, valeur: 11.0 },
];
// --- Réponses d'erreur standard ---
export const ERROR_NOT_FOUND: ApiError = { error: "Ressource introuvable" };
export const ERROR_CONFLICT: ApiError = { error: "Ressource déjà existante" };
export const ERROR_BAD_REQUEST: ApiError = { error: "Requête invalide" };
export const ERROR_UNAUTHORIZED: ApiError = { error: "Non authentifié" };
export const ERROR_FORBIDDEN: ApiError = { error: "Accès interdit" };
+55
View File
@@ -0,0 +1,55 @@
// Setup happy-dom + wrapper render pour les tests de composants Preact
import { Window } from "happy-dom";
let _window: Window | null = null;
/**
* Initialise un environnement DOM virtuel via happy-dom.
* À appeler avant de rendre des composants Preact dans les tests.
*/
export function setupDOM(): void {
_window = new Window({ url: "http://localhost" });
// Expose les globals DOM nécessaires à Preact
const globals = _window as unknown as Record<string, unknown>;
const target = globalThis as unknown as Record<string, unknown>;
for (
const key of [
"document",
"navigator",
"location",
"HTMLElement",
"HTMLInputElement",
"HTMLTextAreaElement",
"HTMLSelectElement",
"Event",
"CustomEvent",
"KeyboardEvent",
"MouseEvent",
"InputEvent",
"MutationObserver",
"requestAnimationFrame",
"cancelAnimationFrame",
]
) {
target[key] = globals[key];
}
target["window"] = _window;
}
/**
* Nettoie l'environnement DOM.
* À appeler dans un afterEach ou à la fin d'un test.
*/
export function cleanupDOM(): void {
if (_window) {
const doc = _window.document;
doc.body.innerHTML = "";
doc.head.innerHTML = "";
_window.close();
_window = null;
}
}
View File
+266
View File
@@ -0,0 +1,266 @@
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,
modules,
notes,
type Student,
students,
} from "../helpers/fixtures.ts";
import { cleanupDOM, setupDOM } from "../helpers/render.ts";
// --- Fixtures ---
Deno.test("fixtures - students match API shape", () => {
assertEquals(students.length, 3);
assertEquals(students[0].numEtud, 21212006);
assertEquals(students[0].idPromo, "4AFISE25/26");
assertEquals(typeof students[0].idPromo, "string");
});
Deno.test("fixtures - modules have string ids", () => {
assertEquals(modules[0].id, "JIN702C");
assertEquals(typeof modules[0].id, "string");
});
Deno.test("fixtures - notes use decimal values", () => {
assertEquals(notes[0].note, 15.5);
assertEquals(notes[0].idModule, "JIN702C");
});
// --- Mock fetch simple (GET 200) ---
Deno.test("mockFetch - GET returns mocked data", async () => {
mockFetch({ "/students": students });
try {
const res = await fetch("http://localhost/api/students");
assertEquals(res.status, 200);
const data = await res.json();
assertEquals(data.length, 3);
assertEquals(data[0].nom, "Dupont");
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - returns 404 for unknown routes", async () => {
mockFetch({});
try {
const res = await fetch("http://localhost/api/unknown");
assertEquals(res.status, 404);
} finally {
restoreFetch();
}
});
// --- Mock fetch avancé (méthodes + status codes) ---
Deno.test("mockFetch - POST 201 created", async () => {
const newStudent = students[0];
mockFetch({
"/students": { method: "POST", status: 201, body: newStudent },
});
try {
const res = await fetch("http://localhost/api/students", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newStudent),
});
assertEquals(res.status, 201);
const data = await res.json();
assertEquals(data.numEtud, 21212006);
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - DELETE 204 no content", async () => {
mockFetch({
"/students/21212006": { method: "DELETE", status: 204 },
});
try {
const res = await fetch("http://localhost/api/students/21212006", {
method: "DELETE",
});
assertEquals(res.status, 204);
assertEquals(res.body, null);
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - 404 error response", async () => {
mockFetch({
"/students/99999": { status: 404, body: ERROR_NOT_FOUND },
});
try {
const res = await fetch("http://localhost/api/students/99999");
assertEquals(res.status, 404);
const data = await res.json();
assertEquals(data.error, "Ressource introuvable");
} finally {
restoreFetch();
}
});
Deno.test("mockFetch - 409 conflict", async () => {
mockFetch({
"/enseignements": { method: "POST", status: 409, body: ERROR_CONFLICT },
});
try {
const res = await fetch("http://localhost/api/enseignements", {
method: "POST",
body: JSON.stringify({
idProf: 1,
idModule: "JIN702C",
idPromo: "4AFISE25/26",
}),
});
assertEquals(res.status, 409);
} finally {
restoreFetch();
}
});
// --- getFetchCalls ---
Deno.test("getFetchCalls - tracks all intercepted calls", async () => {
mockFetch({ "/notes": notes });
try {
await fetch("http://localhost/api/notes");
await fetch("http://localhost/api/notes?numEtud=21212006");
const calls = getFetchCalls();
assertEquals(calls.length, 2);
assertEquals(calls[0].method, "GET");
assertEquals(calls[1].url, "http://localhost/api/notes?numEtud=21212006");
} finally {
restoreFetch();
}
});
Deno.test("getFetchCalls - captures POST body", async () => {
mockFetch({ "/notes": { method: "POST", status: 201, body: notes[0] } });
try {
await fetch("http://localhost/api/notes", {
method: "POST",
body: JSON.stringify(notes[0]),
});
const calls = getFetchCalls();
assertEquals(calls.length, 1);
assertEquals(calls[0].method, "POST");
assertEquals((calls[0].body as { note: number }).note, 15.5);
} finally {
restoreFetch();
}
});
// --- Mock DB ---
Deno.test("mockDb - getTable returns seeded rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
assertEquals(db.getTable("students").length, 3);
});
Deno.test("mockDb - findOne by key", () => {
const db = createMockDb({ tables: { students: [...students] } });
const found = db.findOne<Student>("students", (r) => r.numEtud === 21212006);
assertExists(found);
assertEquals(found.nom, "Dupont");
});
Deno.test("mockDb - findOne returns undefined for missing", () => {
const db = createMockDb({ tables: { students: [...students] } });
const found = db.findOne<Student>("students", (r) => r.numEtud === 99999);
assertEquals(found, undefined);
});
Deno.test("mockDb - insert adds a row", () => {
const db = createMockDb({ tables: { students: [] } });
const newStudent: Student = {
numEtud: 21212099,
nom: "Test",
prenom: "User",
idPromo: "4AFISE25/26",
};
db.insert("students", newStudent);
assertEquals(db.getTable("students").length, 1);
assertEquals(
db.findOne<Student>("students", (r) => r.numEtud === 21212099)?.nom,
"Test",
);
});
Deno.test("mockDb - updateWhere modifies matching rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
const updated = db.updateWhere<Student>(
"students",
(r) => r.numEtud === 21212006,
{ prenom: "Marie" },
);
assertEquals(updated, 1);
assertEquals(
db.findOne<Student>("students", (r) => r.numEtud === 21212006)?.prenom,
"Marie",
);
});
Deno.test("mockDb - deleteWhere removes matching rows", () => {
const db = createMockDb({ tables: { students: [...students] } });
const deleted = db.deleteWhere<Student>(
"students",
(r) => r.numEtud === 21212006,
);
assertEquals(deleted, 1);
assertEquals(db.getTable("students").length, 2);
});
Deno.test("mockDb - findMany with filter", () => {
const db = createMockDb({ tables: { students: [...students] } });
const promo4 = db.findMany<Student>(
"students",
(r) => r.idPromo === "4AFISE25/26",
);
assertEquals(promo4.length, 2);
});
Deno.test("mockDb - reset clears all tables", () => {
const db = createMockDb({
tables: { students: [...students], notes: [...notes] },
});
db.reset();
assertEquals(db.getTable("students").length, 0);
assertEquals(db.getTable("notes").length, 0);
});
Deno.test("mockDb - isolated between instances", () => {
const db1 = createMockDb({ tables: { students: [...students] } });
const db2 = createMockDb({ tables: { students: [...students] } });
db1.deleteWhere<Student>("students", () => true);
assertEquals(db1.getTable("students").length, 0);
assertEquals(db2.getTable("students").length, 3);
});
// --- happy-dom ---
Deno.test({
name: "happy-dom - document is available after setup",
sanitizeResources: false,
sanitizeOps: false,
fn() {
setupDOM();
try {
const doc = globalThis.document;
assertExists(doc);
const div = doc.createElement("div");
div.textContent = "hello";
doc.body.appendChild(div);
assertEquals(doc.body.textContent, "hello");
} finally {
cleanupDOM();
}
},
});
+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