Compare commits

..

7 Commits

Author SHA1 Message Date
djalim 080f7606a7 refactor(api_mock.ts): remove async from mockFetch to match signature
Check Deno code / Check Deno code (pull_request) Successful in 7s
Build and push image / Build Docker image (push) Successful in 2m11s
2026-04-21 12:04:10 +02:00
djalim 4e220f72d7 style: format api mock return type and test imports/JSON body 2026-04-21 12:02:55 +02:00
djalim 61207e4f21 test: add mock DB helper for unit tests
Check Deno code / Check Deno code (pull_request) Failing after 39s
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 11:49:30 +02:00
djalim 204a590b37 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 11:31:45 +02:00
djalim edb20db2ef test: add e2e, integration, and unit tests for fixtures and mockFetch 2026-04-21 11:24:02 +02:00
djalim 56430f9991 test: add API mock, fixtures, and DOM helpers for tests 2026-04-21 11:23:21 +02:00
djalim 808bf8c9c7 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 11:22:09 +02:00
58 changed files with 250 additions and 5336 deletions
-17
View File
@@ -6,26 +6,9 @@ on:
- main - main
jobs: jobs:
check-code:
name: "Check Deno code"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- name: Check formatting
run: deno fmt --check
- name: Check linting
run: deno lint
deploy: deploy:
name: "Build Docker image" name: "Build Docker image"
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: check-code
steps: steps:
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@v3 uses: docker/login-action@v3
+3 -4
View File
@@ -4,10 +4,6 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
- develop
push:
branches:
- develop
permissions: permissions:
contents: read contents: read
@@ -28,3 +24,6 @@ jobs:
- name: Check linting - name: Check linting
run: deno lint run: deno lint
- name: Run tests
run: deno test -A --no-check tests/
-79
View File
@@ -1,79 +0,0 @@
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
- name: Run e2e 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:e2e
-79
View File
@@ -1,79 +0,0 @@
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
- name: Run e2e 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:e2e
-338
View File
@@ -1,338 +0,0 @@
# PolyMPR - Claude Code Context
## 📋 Project Overview
**PolyMPR** (Poly Management Platform for Resources) is a modular HR management
system built with **Deno + Fresh** framework. It's designed to help
organizations manage HR, student records, notes, mobility programs, and
role-based administration.
### Stack
- **Runtime**: Deno
- **Framework**: Fresh (edge-ready web framework)
- **Database**: PostgreSQL with Drizzle ORM
- **Frontend**: Preact with signals
- **Authentication**: JWT-based via cookies
- **Testing**: Deno test framework with HappyDOM for DOM testing
### Current Status
🚧 **In Progress** - Application is far from complete. The schema below is the
**final/definitive schema** that should guide all development.
---
## 🏗️ Architecture
### Module Structure
The application uses a **modulith architecture** with the following modules:
```
routes/(apps)/
├── students/ - Student management & promotions
├── notes/ - Grade management & academic records
├── mobility/ - Mobility programs & exchanges
└── admin/ - Role & permission management
```
### Key Directories
- `/routes` - Fresh routes and components
- `/databases` - Database connection, schema, and migrations
- `/defaults` - Interfaces and shared types
- `/tests` - Unit, integration, and E2E tests
- `/static` - Public assets
### Authentication Flow
1. User authenticates via CAS (Polytech)
2. JWT token stored in `sessionToken` cookie
3. Middleware validates token on each request
4. Public routes: `/`, `/login`, `/logout`, `/about`, `/contact`
5. All other routes require authentication
---
## 📊 Database Schema (Final/Definitive)
```mermaid
erDiagram
USER {
string id PK
string nom
string prenom
int idRole FK
}
ROLE {
int id PK
string nom
}
PERMISSION {
int id PK
string nom
}
ROLE_PERMISSION {
int idRole PK,FK
int idPermission PK,FK
}
STUDENT {
int numEtud PK
string nom
string prenom
string idPromo FK
}
PROMOTION {
string idPromo PK
string annee
}
MODULE {
string id PK
string nom
}
ENSEIGNEMENT {
string idProf PK,FK
string idModule PK,FK
string idPromo PK,FK
}
UE {
int id PK
string nom
}
UE_MODULE {
string idModule PK,FK
int idUE PK,FK
string idPromo PK,FK
float coeff
}
NOTE {
int numEtud PK,FK
string idModule PK,FK
float note
}
AJUSTEMENT {
int numEtud PK,FK
int idUE PK,FK
float valeur
}
USER }o--|| ROLE : "a"
ROLE_PERMISSION }o--|| ROLE : "accorde"
ROLE_PERMISSION }o--|| PERMISSION : "inclut"
ENSEIGNEMENT }o--|| USER : "réalisé par"
ENSEIGNEMENT }o--|| MODULE : "porte sur"
ENSEIGNEMENT }o--|| PROMOTION : "concerne"
STUDENT }o--|| PROMOTION : "appartient à"
UE_MODULE }o--|| MODULE : "associe"
UE_MODULE }o--|| UE : "appartient à"
UE_MODULE }o--|| PROMOTION : "pour"
NOTE }o--|| STUDENT : "reçoit"
NOTE }o--|| MODULE : "dans"
AJUSTEMENT }o--|| STUDENT : "concerne"
AJUSTEMENT }o--|| UE : "dans"
```
### Current Schema (Incomplete)
The current Drizzle ORM schema in `/databases/schema.ts` only implements:
- `promotions`
- `students`
- `mobility`
**Migration needed**: Update schema to match the final ER diagram above.
---
## 🎯 Open Issues (69 total)
### UI Pages
**Catalog**
- 📋 UI - Page Catalogue d'applications (#71)
**Components**
- 🎨 UI (composant) - Popup Résultats d'import (#75)
**Students**
- 📋 UI - Admin Liste des élèves (#79)
- 📋 UI - Admin Gestion des promotions (#80)
- 📋 UI - Admin Import xlsx élèves (#81)
- 📋 UI - Admin Édition d'un élève (#82)
**Notes**
- 📋 UI - Page Élève Mes Notes (#72)
- 📋 UI - Admin Consulter les notes (#73)
- 📋 UI - Admin Importer des notes (.xlsx) (#74)
- 📋 UI - Admin Édition notes d'un élève (#76)
- 📋 UI - Admin Récap notes élève / semestre (#77)
- 📋 UI - Admin Gestion des UEs (#78)
**Administration**
- 📋 UI - Gestion des utilisateurs (#83)
- 📋 UI - Gestion des rôles (#84)
- 📋 UI - Permissions d'un rôle (#85)
- 📋 UI - Vue des permissions (#86)
- 📋 UI - Gestion des modules (#87)
- 📋 UI - Enseignements (Assignations) (#88)
---
### API Endpoints
**Students API**
- 📋 GET `/students` (#7)
- 📋 POST `/students` (#8)
- 📋 POST `/students/import-csv` (#9)
- 📋 GET `/students/{numEtud}` (#10)
- 📋 PUT `/students/{numEtud}` (#11)
- 📋 DELETE `/students/{numEtud}` (#12)
- 📋 GET `/promotions` (#13)
- 📋 POST `/promotions` (#14)
- 📋 GET `/promotions/{idPromo}` (#15)
- 📋 PUT `/promotions/{idPromo}` (#16)
- 📋 DELETE `/promotions/{idPromo}` (#17)
**Administration API - Modules & Enseignements**
- 📋 GET `/modules` (#23)
- 📋 POST `/modules` (#24)
- 📋 GET `/modules/{idModule}` (#25)
- 📋 PUT `/modules/{idModule}` (#26)
- 📋 DELETE `/modules/{idModule}` (#27)
- 📋 POST `/enseignements` (#29)
- 📋 GET `/enseignements/{idProf}/{idModule}/{idPromo}` (#30)
- 📋 DELETE `/enseignements/{idProf}/{idModule}/{idPromo}` (#31)
**Notes API - UEs & UE-Modules**
- 📋 GET `/ues` (#32)
- 📋 POST `/ues` (#33)
- 📋 GET `/ues/{idUE}` (#34)
- 📋 PUT `/ues/{idUE}` (#35)
- 📋 DELETE `/ues/{idUE}` (#36)
- 📋 GET `/ue-modules` (#37)
- 📋 POST `/ue-modules` (#38)
- 📋 GET `/ue-modules/{idModule}/{idUE}/{idPromo}` (#39)
- 📋 PUT `/ue-modules/{idModule}/{idUE}/{idPromo}` (#40)
- 📋 DELETE `/ue-modules/{idModule}/{idUE}/{idPromo}` (#41)
**Notes API - Notes & Ajustements**
- 📋 GET `/notes` (#42)
- 📋 POST `/notes` (#43)
- 📋 POST `/notes/import-xlsx` (#44)
- 📋 GET `/notes/{numEtud}/{idModule}` (#45)
- 📋 PUT `/notes/{numEtud}/{idModule}` (#46)
- 📋 DELETE `/notes/{numEtud}/{idModule}` (#47)
- 📋 GET `/ajustements` (#48)
- 📋 POST `/ajustements` (#49)
- 📋 GET `/ajustements/{numEtud}/{idUE}` (#50)
- 📋 PUT `/ajustements/{numEtud}/{idUE}` (#51)
- 📋 DELETE `/ajustements/{numEtud}/{idUE}` (#52)
**Administration API - Users, Roles & Permissions**
- 📋 GET `/users` (#60)
- 📋 POST `/users` (#61)
- 📋 GET `/users/{id}` (#62)
- 📋 PUT `/users/{id}` (#63)
- 📋 DELETE `/users/{id}` (#64)
- 📋 GET `/roles` (#65)
- 📋 POST `/roles` (#66)
- 📋 GET `/roles/{idRole}` (#67)
- 📋 PUT `/roles/{idRole}` (#68)
- 📋 DELETE `/roles/{idRole}` (#69)
- 📋 GET `/permissions` (#70)
---
## 🎨 Design Reference
**Figma Prototype**:
https://www.figma.com/design/La79bsUsWnJCtMsrrt2zGd/Prototype?node-id=0-1
This is the **final design specification** for the UI. All UI implementations
should follow this design.
---
## 🚀 Development Guidelines
### Getting Started
```bash
# Run tests
deno task test
# Start development server
deno task start
# Build for production
deno task build
# Format & lint
deno task check
```
### Git Workflow
1. Create branch: `git checkout -b PMPR-{ISSUE_ID}`
2. Implement changes
3. Run tests and linting
4. Submit PR
### Code Style
- Format: Follow Deno defaults (enforced via `deno fmt`)
- Linting: Fresh recommended rules
- TypeScript strict mode enabled
- Use Drizzle ORM for database operations
### Testing
- Write unit tests for business logic
- Integration tests for API endpoints
- E2E tests with HappyDOM for UI interactions
- Mock database with provided helpers
---
## 📦 Key Dependencies
- **fresh@1.7.3** - Web framework
- **drizzle-orm@0.45.2** - ORM
- **pg@8.20.0** - PostgreSQL driver
- **@popov/jwt@1.0.1** - JWT utilities
- **preact@10.22.0** - UI library
- **happy-dom@16.0.0** - DOM testing
---
## 🔗 Related Resources
- **Repository**: https://git.polytech.djalim.fr/djalim/PolyMPR
- **Issue Tracker**: Gitea (via `tea` CLI)
- **Wiki**: Check CONTRIBUTING.md for dev setup
- **Database**: PostgreSQL (configured in `.env`)
---
## 💡 Important Notes
1. **Current Limitation**: The database schema in `/databases/schema.ts` does
NOT match the final ER diagram. This is a priority migration task.
2. **Design System**: Follow the Figma prototype for all UI work.
3. **Module Pattern**: Each module should follow the same pattern: routes, API
endpoints, components, and tests.
4. **Permissions**: All admin operations should respect the ROLE_PERMISSION
system.
5. **Fresh Conventions**: Routes use Fresh's file-based routing convention
(e.g., `routes/path/index.tsx`).
-158
View File
@@ -1,158 +0,0 @@
# 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
@@ -1,233 +0,0 @@
{
"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=="],
}
}
+5 -19
View File
@@ -1,24 +1,10 @@
services: services:
app: app:
image: registry.docker.polytech.djalim.fr/polympr:latest container_name: deno_fresh_app
build: .
ports: ports:
- "8008:80" - "80:80"
- "4430:443" - "443:443"
volumes: volumes:
- /home/kevin/PolyMPR/:/app - .:/app
command: deno run -A main.ts 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
@@ -1,14 +0,0 @@
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);
@@ -1,100 +0,0 @@
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;
@@ -1,680 +0,0 @@
{
"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
@@ -1,13 +0,0 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1777155028708,
"tag": "0000_square_jetstream",
"breakpoints": true
}
]
}
-99
View File
@@ -1,99 +0,0 @@
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
@@ -1,99 +0,0 @@
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
@@ -10,11 +10,7 @@
"build": "deno run -A --unstable-ffi dev.ts build", "build": "deno run -A --unstable-ffi dev.ts build",
"preview": "deno run -A --unstable-ffi main.ts", "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": "deno test -A --no-check tests/"
"test:unit": "deno test -A --no-check tests/unit/",
"test:integration": "deno test -A --no-check tests/integration/",
"test:e2e": "deno test -A --no-check tests/e2e/",
"migrate": "node_modules/.bin/drizzle-kit migrate"
}, },
"lint": { "lint": {
"rules": { "rules": {
-17
View File
@@ -1,17 +0,0 @@
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,
},
});
-6
View File
@@ -1,8 +1,2 @@
#Local mode, set to true to access admin pages with any users #Local mode, set to true to access admin pages with any users
LOCAL=false LOCAL=false
POSTGRES_HOST = db
POSTGRES_PORT = 5432
POSTGRES_PASS = astrongpass
POSTGRES_USER = postgres
POSTGRES_DB = polympr
Generated
-61
View File
@@ -1,61 +0,0 @@
{
"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
@@ -1,62 +0,0 @@
{
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."
'';
};
}
);
}
-12
View File
@@ -1,12 +0,0 @@
{
"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"
}
}
-13
View File
@@ -1,13 +0,0 @@
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
@@ -1,70 +0,0 @@
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" },
});
},
};
@@ -1,75 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,63 +0,0 @@
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" },
});
},
};
@@ -1,65 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,68 +0,0 @@
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
@@ -1,101 +0,0 @@
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
@@ -1,67 +0,0 @@
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
@@ -1,66 +0,0 @@
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
@@ -1,2 +0,0 @@
import makeIndex from "$root/defaults/makeIndex.ts";
export default makeIndex(import.meta.dirname!);
-13
View File
@@ -1,13 +0,0 @@
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);
+110 -64
View File
@@ -1,40 +1,55 @@
import { Handlers } from "$fresh/server.ts"; import { Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import { Database } from "@db/sqlite";
import { mobility, promotions, students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
export const handler: Handlers = { export const handler: Handlers = {
// deno-lint-ignore require-await
async GET() { async GET() {
try { try {
const studentRows = await db console.log("Connecting to mobility database...");
.select({ const connection = new Database("databases/data/mobility.db", {
id: students.userId, create: false,
firstName: students.firstName, });
lastName: students.lastName, connection.run(
promotionId: students.promotionId, "ATTACH DATABASE 'databases/data/students.db' AS students",
endyear: promotions.endyear, );
current: promotions.current, console.log("Connected to databases.");
})
.from(students)
.leftJoin(promotions, eq(students.promotionId, promotions.id));
const mobilityRows = await db.select().from(mobility); 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 promotionRows = await db const mobilities = connection.prepare(
.select({ `SELECT
id: promotions.id, mobility.id,
endyear: promotions.endyear, mobility.studentId,
current: promotions.current, mobility.startDate,
}) mobility.endDate,
.from(promotions); mobility.weeksCount,
mobility.destinationCountry,
mobility.destinationName,
mobility.mobilityStatus
FROM mobility`,
).all();
const promotions = connection.prepare(
`SELECT id, name FROM students.promotions`,
).all();
connection.close();
return new Response( return new Response(
JSON.stringify({ JSON.stringify({ mobilities, students, promotions }),
mobilities: mobilityRows, {
students: studentRows, status: 200,
promotions: promotionRows, headers: { "Content-Type": "application/json" },
}), },
{ status: 200, headers: { "Content-Type": "application/json" } },
); );
} catch (error) { } catch (error) {
console.error("Error fetching mobility data:", error); console.error("Error fetching mobility data:", error);
@@ -43,6 +58,8 @@ export const handler: Handlers = {
}, },
async POST(request) { async POST(request) {
console.log("API /mobility/api/insert_mobility POST called");
try { try {
const body = await request.json(); const body = await request.json();
const { data } = body; const { data } = body;
@@ -50,8 +67,32 @@ export const handler: Handlers = {
if (!Array.isArray(data)) { if (!Array.isArray(data)) {
throw new Error("Invalid request body"); throw new Error("Invalid request body");
} }
console.log("Connecting to mobility database...");
const connection = new Database("databases/data/mobility.db", {
create: false,
});
for (const entry of data) { 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) {
const { const {
id, id,
studentId, studentId,
@@ -61,16 +102,19 @@ export const handler: Handlers = {
destinationCountry, destinationCountry,
destinationName, destinationName,
mobilityStatus = "N/A", mobilityStatus = "N/A",
} = entry; } = mobility;
const studentExists = await db console.log("Processing mobility data:", mobility);
.select({ userId: students.userId })
.from(students)
.where(eq(students.userId, studentId))
.limit(1)
.then((rows) => rows.length > 0);
if (!studentExists) { 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) {
console.warn(`Skipping mobility for unknown studentId: ${studentId}`); console.warn(`Skipping mobility for unknown studentId: ${studentId}`);
continue; continue;
} }
@@ -79,38 +123,40 @@ export const handler: Handlers = {
if (startDate && endDate) { if (startDate && endDate) {
const start = new Date(startDate); const start = new Date(startDate);
const end = new Date(endDate); const end = new Date(endDate);
calculatedWeeksCount = start <= end if (start <= end) {
? Math.ceil( calculatedWeeksCount = Math.ceil(
(end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000), (end.getTime() - start.getTime()) / (7 * 24 * 60 * 60 * 1000),
) );
: null; } else {
calculatedWeeksCount = null;
}
} }
await db console.log("Executing SQL insert/update query for:", {
.insert(mobility) id,
.values({ studentId,
id, startDate,
studentId, endDate,
startDate, calculatedWeeksCount,
endDate, destinationCountry,
weeksCount: calculatedWeeksCount, destinationName,
destinationCountry, mobilityStatus,
destinationName, });
mobilityStatus,
}) insertQuery.run(
.onConflictDoUpdate({ id,
target: mobility.id, studentId,
set: { startDate,
startDate, endDate,
endDate, calculatedWeeksCount,
weeksCount: calculatedWeeksCount, destinationCountry,
destinationCountry, destinationName,
destinationName, mobilityStatus,
mobilityStatus, );
},
});
} }
connection.close();
console.log("Mobility data inserted/updated successfully.");
return new Response("Data inserted/updated successfully", { return new Response("Data inserted/updated successfully", {
status: 200, status: 200,
}); });
-83
View File
@@ -1,83 +0,0 @@
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 });
}
},
};
@@ -1,107 +0,0 @@
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
@@ -1,64 +0,0 @@
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 });
}
},
};
@@ -1,139 +0,0 @@
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
@@ -1,66 +0,0 @@
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 });
}
},
};
@@ -1,139 +0,0 @@
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
@@ -1,42 +0,0 @@
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
@@ -1,122 +0,0 @@
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 });
}
},
};
-49
View File
@@ -1,49 +0,0 @@
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" },
});
},
};
@@ -1,79 +0,0 @@
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 });
},
};
+131 -41
View File
@@ -1,61 +1,151 @@
import { FreshContext, Handlers } from "$fresh/server.ts"; import { FreshContext, Handlers } from "$fresh/server.ts";
import { db } from "$root/databases/db.ts"; import connect from "$root/databases/connect.ts";
import { students } from "$root/databases/schema.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts"; import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { eq } from "npm:drizzle-orm@0.45.2"; 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,
);
}
}
export const handler: Handlers<null, AuthenticatedState> = { export const handler: Handlers<null, AuthenticatedState> = {
// #7 GET /students /**
* The students the user can see.
* @param _request The HTTP request.
* @param _context The context with authenticated state.
* @returns All students our user can see.
*/
// deno-lint-ignore require-await
async GET( async GET(
request: Request, _request: Request,
context: FreshContext<AuthenticatedState>, context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { using connection = connect("students");
return new Response(JSON.stringify([]), { const database = connection.database;
headers: { "content-type": "application/json" },
}); if (context.state.session.eduPersonPrimaryAffiliation == "student") {
return new Response(
JSON.stringify(getItself(database, context.state.session.uid)),
{
headers: {
"content-type": "application/json",
},
},
);
} }
const url = new URL(request.url); return new Response(
const idPromo = url.searchParams.get("idPromo"); JSON.stringify(getAll(database)),
{
const rows = idPromo headers: {
? await db.select().from(students).where(eq(students.idPromo, idPromo)) "content-type": "application/json",
: await db.select().from(students); },
},
return new Response(JSON.stringify(rows), { );
headers: { "content-type": "application/json" },
});
}, },
/**
// #8 POST /students * Add students in the database.
* @param request The HTTP request.
* @param _context The Fresh context.
* @returns HTTP 201 on successful insert.
*/
async POST( async POST(
request: Request, request: Request,
context: FreshContext<AuthenticatedState>, _context: FreshContext<AuthenticatedState>,
): Promise<Response> { ): Promise<Response> {
if (context.state.session.eduPersonPrimaryAffiliation !== "employee") { const { students, promo }: { students: Student[]; promo: string } =
return new Response(null, { status: 403 }); await request.json();
}
const body: { if (!promo || !promo.match(/^\d{4}-\dA$/) || !Array.isArray(students)) {
numEtud: number;
nom: string;
prenom: string;
idPromo: string;
} = await request.json();
if (!body.nom || !body.prenom || !body.idPromo) {
return new Response(null, { status: 400 }); return new Response(null, { status: 400 });
} }
const [created] = await db using connection = connect("students");
.insert(students) const database = connection.database;
.values({ nom: body.nom, prenom: body.prenom, idPromo: body.idPromo })
.returning();
return new Response(JSON.stringify(created), { const { endyear, current } = promo.match(
status: 201, /^(?<endyear>\d{4})-(?<current>\d)A$/,
headers: { "content-type": "application/json" }, )?.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 });
}, },
}; };
@@ -1,83 +0,0 @@
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 });
},
};
@@ -1,64 +0,0 @@
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" },
});
},
};
-23
View File
@@ -1,23 +0,0 @@
{ 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."
'';
}
-212
View File
@@ -1,212 +0,0 @@
// #110 - E2E tests for /promotions endpoints
import { assertEquals } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import { seedPromotions, truncateAll } from "../helpers/db_integration.ts";
import { handler as promotionsHandler } from "$apps/students/api/promotions.ts";
import { handler as promotionHandler } from "$apps/students/api/promotions/[idPromo].ts";
// --- GET /promotions ---
Deno.test({
name: "e2e promotions: GET /promotions returns all as employee",
async fn() {
await truncateAll();
await seedPromotions([
{ id: "PEIP1-2024", annee: "2024" },
{ id: "PEIP2-2024", annee: "2024" },
]);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeEmployeeContext(),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions returns empty for non-employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024", annee: "2024" }]);
const res = await promotionsHandler.GET!(
makeGetRequest("/promotions"),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /promotions ---
Deno.test({
name: "e2e promotions: POST /promotions creates promotion (201)",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", {
idPromo: "INFO3-2025",
annee: "2025",
}),
makeEmployeeContext(),
);
assertEquals(res.status, 201);
const body = await res.json();
assertEquals(body.id, "INFO3-2025");
assertEquals(body.annee, "2025");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: POST /promotions 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X", annee: "2025" }),
makeContextWithAffiliation("student"),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: POST /promotions 400 on missing fields",
async fn() {
await truncateAll();
const res = await promotionsHandler.POST!(
makeJsonRequest("/promotions", "POST", { idPromo: "X" }),
makeEmployeeContext(),
);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: GET /promotions/:id returns promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024", annee: "2024" }]);
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeEmployeeContext({ idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.id, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: GET /promotions/:id 403 for non-employee",
async fn() {
await truncateAll();
const res = await promotionHandler.GET!(
makeGetRequest("/promotions/INFO3-2024"),
makeContextWithAffiliation("student", { idPromo: "INFO3-2024" }),
);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: PUT /promotions/:id updates annee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2023", annee: "2023" }]);
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/INFO3-2023", "PUT", { annee: "2024" }),
makeEmployeeContext({ idPromo: "INFO3-2023" }),
);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.annee, "2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: PUT /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.PUT!(
makeJsonRequest("/promotions/GHOST", "PUT", { annee: "2025" }),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /promotions/:idPromo ---
Deno.test({
name: "e2e promotions: DELETE /promotions/:id returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]);
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/INFO3-2022"),
makeEmployeeContext({ idPromo: "INFO3-2022" }),
);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e promotions: DELETE /promotions/:id 404 when not found",
async fn() {
await truncateAll();
const res = await promotionHandler.DELETE!(
makeGetRequest("/promotions/GHOST"),
makeEmployeeContext({ idPromo: "GHOST" }),
);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-288
View File
@@ -1,288 +0,0 @@
// #109 - E2E tests for /students endpoints
// Appelle les handlers Fresh directement avec un vrai contexte + vraie DB
import { assertEquals, assertExists } from "@std/assert";
import {
makeContextWithAffiliation,
makeEmployeeContext,
makeGetRequest,
makeJsonRequest,
} from "../helpers/handler.ts";
import {
seedPromotions,
seedStudents,
truncateAll,
} from "../helpers/db_integration.ts";
import { handler as studentsHandler } from "$apps/students/api/students.ts";
import { handler as studentHandler } from "$apps/students/api/students/[numEtud].ts";
// --- GET /students ---
Deno.test({
name: "e2e students: GET /students returns all students as employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
]);
const req = makeGetRequest("/students");
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertExists(body.find((s: { nom: string }) => s.nom === "Dupont"));
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students returns empty array for non-employee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
]);
const req = makeGetRequest("/students");
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students?idPromo filters by promotion",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }, { id: "PEIP2-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
{ nom: "Durand", prenom: "Claire", idPromo: "PEIP2-2024" },
]);
const req = makeGetRequest("/students", { idPromo: "PEIP1-2024" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.length, 2);
assertEquals(
body.every((s: { idPromo: string }) => s.idPromo === "PEIP1-2024"),
true,
);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- POST /students ---
Deno.test({
name: "e2e students: POST /students creates a student (201)",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const req = makeJsonRequest("/students", "POST", {
nom: "Leroy",
prenom: "Paul",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 201);
const body = await res.json();
assertExists(body.numEtud);
assertEquals(body.nom, "Leroy");
assertEquals(body.idPromo, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: POST /students 403 for non-employee",
async fn() {
await truncateAll();
const req = makeJsonRequest("/students", "POST", {
nom: "Test",
prenom: "User",
idPromo: "PEIP1-2024",
});
const ctx = makeContextWithAffiliation("student");
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: POST /students 400 when missing required fields",
async fn() {
await truncateAll();
const req = makeJsonRequest("/students", "POST", { nom: "Leroy" });
const ctx = makeEmployeeContext();
const res = await studentsHandler.POST!(req, ctx);
assertEquals(res.status, 400);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- GET /students/:numEtud ---
Deno.test({
name: "e2e students: GET /students/:numEtud returns student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Bernard", prenom: "Lucie", idPromo: "INFO3-2024" },
]);
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.numEtud, s.numEtud);
assertEquals(body.nom, "Bernard");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: GET /students/:numEtud 403 for non-employee",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/12345");
const ctx = makeContextWithAffiliation("student", { numEtud: "12345" });
const res = await studentHandler.GET!(req, ctx);
assertEquals(res.status, 403);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- PUT /students/:numEtud ---
Deno.test({
name: "e2e students: PUT /students/:numEtud updates student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]);
const [s] = await seedStudents([
{ nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" },
]);
const req = makeJsonRequest(`/students/${s.numEtud}`, "PUT", {
nom: "Grand",
prenom: "Hugo",
idPromo: "INFO4-2024",
});
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(res.status, 200);
const body = await res.json();
assertEquals(body.nom, "Grand");
assertEquals(body.idPromo, "INFO4-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: PUT /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const req = makeJsonRequest("/students/999999", "PUT", {
nom: "Ghost",
prenom: "Ghost",
idPromo: "INFO3-2024",
});
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.PUT!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
// --- DELETE /students/:numEtud ---
Deno.test({
name: "e2e students: DELETE /students/:numEtud returns 204",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" },
]);
const req = makeGetRequest(`/students/${s.numEtud}`);
const ctx = makeEmployeeContext({ numEtud: String(s.numEtud) });
const res = await studentHandler.DELETE!(req, ctx);
assertEquals(res.status, 204);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "e2e students: DELETE /students/:numEtud 404 when not found",
async fn() {
await truncateAll();
const req = makeGetRequest("/students/999999");
const ctx = makeEmployeeContext({ numEtud: "999999" });
const res = await studentHandler.DELETE!(req, ctx);
assertEquals(res.status, 404);
},
sanitizeResources: false,
sanitizeOps: false,
});
-89
View File
@@ -1,89 +0,0 @@
// 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();
}
-88
View File
@@ -1,88 +0,0 @@
// Helper pour les tests E2E — appel direct des handlers Fresh
// sans lancer de serveur HTTP
import { FreshContext } from "$fresh/server.ts";
import { AuthenticatedState } from "$root/defaults/interfaces.ts";
import { CasContent } from "$root/defaults/interfaces.ts";
const BASE_EMPLOYEE_SESSION: CasContent = {
amuCampus: "",
amuComposante: "",
amuDateValidation: "",
coGroup: "",
eduPersonPrimaryAffiliation: "employee",
eduPersonPrincipalName: "test.user@polytech.fr",
mail: "test.user@polytech.fr",
displayName: "Test User",
givenName: "Test",
memberOf: [],
sn: "User",
supannCivilite: "M.",
supannEntiteAffectation: "",
supannEtuAnneeInscription: "",
supannEtuEtape: "",
uid: "test.user",
};
/**
* Crée un FreshContext mock authentifié en tant qu'employee.
*/
export function makeEmployeeContext(
params: Record<string, string> = {},
): FreshContext<AuthenticatedState> {
return {
params,
state: {
isAuthenticated: true,
session: { ...BASE_EMPLOYEE_SESSION },
availablePages: {},
},
render: () => Promise.resolve(new Response()),
renderNotFound: () => Promise.resolve(new Response(null, { status: 404 })),
next: () => Promise.resolve(new Response()),
} as unknown as FreshContext<AuthenticatedState>;
}
/**
* Crée un FreshContext mock avec un affiliation personnalisée.
*/
export function makeContextWithAffiliation(
affiliation: string,
params: Record<string, string> = {},
): FreshContext<AuthenticatedState> {
const ctx = makeEmployeeContext(params);
(ctx.state as AuthenticatedState).session.eduPersonPrimaryAffiliation =
affiliation;
return ctx;
}
/**
* Crée une Request GET simple.
*/
export function makeGetRequest(
path: string,
searchParams?: Record<string, string>,
): Request {
const url = new URL(`http://localhost${path}`);
if (searchParams) {
for (const [k, v] of Object.entries(searchParams)) {
url.searchParams.set(k, v);
}
}
return new Request(url.toString());
}
/**
* Crée une Request POST/PUT avec un corps JSON.
*/
export function makeJsonRequest(
path: string,
method: string,
body: unknown,
): Request {
return new Request(`http://localhost${path}`, {
method,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
}
-112
View File
@@ -1,112 +0,0 @@
// #110 - Integration tests for /promotions endpoints
import { assertEquals, assertExists } from "@std/assert";
import {
seedPromotions,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { promotions } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
Deno.test({
name: "integration promotions: list all",
async fn() {
await truncateAll();
await seedPromotions([
{ id: "PEIP1-2024", annee: "2024" },
{ id: "PEIP2-2024", annee: "2024" },
]);
const rows = await testDb.select().from(promotions);
assertEquals(rows.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: create and retrieve by id",
async fn() {
await truncateAll();
const [created] = await testDb
.insert(promotions)
.values({ id: "INFO3-2025", annee: "2025" })
.returning();
assertExists(created);
assertEquals(created.id, "INFO3-2025");
assertEquals(created.annee, "2025");
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2025"))
.then((r) => r[0] ?? null);
assertExists(row);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: get by id returns null when not found",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "NONEXISTENT"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update annee",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2023", annee: "2023" }]);
const [updated] = await testDb
.update(promotions)
.set({ annee: "2024" })
.where(eq(promotions.id, "INFO3-2023"))
.returning();
assertExists(updated);
assertEquals(updated.annee, "2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: delete removes the row",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2022", annee: "2022" }]);
await testDb.delete(promotions).where(eq(promotions.id, "INFO3-2022"));
const row = await testDb
.select()
.from(promotions)
.where(eq(promotions.id, "INFO3-2022"))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration promotions: update non-existent returns empty",
async fn() {
await truncateAll();
const result = await testDb
.update(promotions)
.set({ annee: "2099" })
.where(eq(promotions.id, "GHOST"))
.returning();
assertEquals(result.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
-173
View File
@@ -1,173 +0,0 @@
// #109 - Integration tests for /students endpoints
// Teste les opérations DB directement avec une vraie base de données
import { assertEquals, assertExists } from "@std/assert";
import {
seedPromotions,
seedStudents,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { students } from "$root/databases/schema.ts";
import { eq } from "npm:drizzle-orm@0.45.2";
Deno.test({
name: "integration students: list all students",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
]);
const rows = await testDb.select().from(students);
assertEquals(rows.length, 2);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: filter by idPromo",
async fn() {
await truncateAll();
await seedPromotions([{ id: "PEIP1-2024" }, { id: "PEIP2-2024" }]);
await seedStudents([
{ nom: "Dupont", prenom: "Jean", idPromo: "PEIP1-2024" },
{ nom: "Martin", prenom: "Alice", idPromo: "PEIP1-2024" },
{ nom: "Durand", prenom: "Claire", idPromo: "PEIP2-2024" },
]);
const rows = await testDb
.select()
.from(students)
.where(eq(students.idPromo, "PEIP1-2024"));
assertEquals(rows.length, 2);
assertEquals(rows.every((s) => s.idPromo === "PEIP1-2024"), true);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: create and retrieve by numEtud",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [created] = await testDb
.insert(students)
.values({ nom: "Leroy", prenom: "Paul", idPromo: "INFO3-2024" })
.returning();
assertExists(created.numEtud);
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, created.numEtud))
.then((r) => r[0] ?? null);
assertExists(row);
assertEquals(row.nom, "Leroy");
assertEquals(row.idPromo, "INFO3-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: get by numEtud returns null when not found",
async fn() {
await truncateAll();
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, 999999))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: update student fields",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }, { id: "INFO4-2024" }]);
const [s] = await seedStudents([
{ nom: "Petit", prenom: "Hugo", idPromo: "INFO3-2024" },
]);
const [updated] = await testDb
.update(students)
.set({ nom: "Grand", idPromo: "INFO4-2024" })
.where(eq(students.numEtud, s.numEtud))
.returning();
assertEquals(updated.nom, "Grand");
assertEquals(updated.idPromo, "INFO4-2024");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: delete student",
async fn() {
await truncateAll();
await seedPromotions([{ id: "INFO3-2024" }]);
const [s] = await seedStudents([
{ nom: "Thomas", prenom: "Eva", idPromo: "INFO3-2024" },
]);
await testDb.delete(students).where(eq(students.numEtud, s.numEtud));
const row = await testDb
.select()
.from(students)
.where(eq(students.numEtud, s.numEtud))
.then((r) => r[0] ?? null);
assertEquals(row, null);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: update non-existent student returns empty",
async fn() {
await truncateAll();
const result = await testDb
.update(students)
.set({ nom: "Ghost" })
.where(eq(students.numEtud, 999999))
.returning();
assertEquals(result.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration students: delete non-existent student returns empty",
async fn() {
await truncateAll();
const result = await testDb
.delete(students)
.where(eq(students.numEtud, 999999))
.returning();
assertEquals(result.length, 0);
},
sanitizeResources: false,
sanitizeOps: false,
});
-58
View File
@@ -1,58 +0,0 @@
import { assertEquals, assertExists } from "@std/assert";
import {
closeTestPool,
seedRoles,
seedUsers,
testDb,
truncateAll,
} from "../helpers/db_integration.ts";
import { users } from "$root/databases/schema.ts";
Deno.test({
name: "integration: GET /users - DB round trip",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "employee" }]);
await seedUsers([
{ id: "dupont.jean", nom: "Dupont", prenom: "Jean", idRole: role.id },
{ id: "martin.alice", nom: "Martin", prenom: "Alice", idRole: role.id },
]);
const rows = await testDb.select().from(users);
assertEquals(rows.length, 2);
assertExists(rows.find((u) => u.id === "dupont.jean"));
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration: INSERT user and retrieve by id",
async fn() {
await truncateAll();
const [role] = await seedRoles([{ nom: "admin" }]);
const [created] = await testDb.insert(users).values({
id: "durand.claire",
nom: "Durand",
prenom: "Claire",
idRole: role.id,
}).returning();
assertExists(created);
assertEquals(created.id, "durand.claire");
assertEquals(created.nom, "Durand");
},
sanitizeResources: false,
sanitizeOps: false,
});
Deno.test({
name: "integration: cleanup - close pool",
async fn() {
await closeTestPool();
},
sanitizeResources: false,
sanitizeOps: false,
});
-160
View File
@@ -1,160 +0,0 @@
// #110 - Unit tests for /promotions endpoints
import { assertEquals, assertExists } from "@std/assert";
import { mockFetch, restoreFetch } from "../helpers/api_mock.ts";
import { createMockDb } from "../helpers/db_mock.ts";
import { type Promotion, promotions } from "../helpers/fixtures.ts";
// --- Fixtures ---
Deno.test("promotions: fixtures have correct shape", () => {
assertEquals(promotions.length, 3);
assertEquals(typeof promotions[0].idPromo, "string");
assertEquals(typeof promotions[0].annee, "string");
});
// --- Mock API ---
Deno.test("mock API: GET /promotions returns list", async () => {
mockFetch({ "/promotions": promotions });
try {
const res = await fetch("http://localhost/api/promotions");
assertEquals(res.status, 200);
const data: Promotion[] = await res.json();
assertEquals(data.length, 3);
assertExists(data.find((p) => p.idPromo === "4AFISE25/26"));
} finally {
restoreFetch();
}
});
Deno.test("mock API: GET /promotions/:id returns one", async () => {
mockFetch({ "/promotions/4AFISE25%2F26": promotions[0] });
try {
const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26");
assertEquals(res.status, 200);
const data: Promotion = await res.json();
assertEquals(data.idPromo, "4AFISE25/26");
} finally {
restoreFetch();
}
});
Deno.test("mock API: GET /promotions/:id 404 when not found", async () => {
mockFetch({
"/promotions/UNKNOWN": {
status: 404,
body: { error: "Ressource introuvable" },
},
});
try {
const res = await fetch("http://localhost/api/promotions/UNKNOWN");
assertEquals(res.status, 404);
} finally {
restoreFetch();
}
});
Deno.test("mock API: POST /promotions creates promotion (201)", async () => {
const newPromo: Promotion = { idPromo: "NEW2025", annee: "2025" };
mockFetch({ "/promotions": { method: "POST", status: 201, body: newPromo } });
try {
const res = await fetch("http://localhost/api/promotions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idPromo: "NEW2025", annee: "2025" }),
});
assertEquals(res.status, 201);
const data: Promotion = await res.json();
assertEquals(data.idPromo, "NEW2025");
} finally {
restoreFetch();
}
});
Deno.test("mock API: POST /promotions 400 on missing fields", async () => {
mockFetch({ "/promotions": { method: "POST", status: 400 } });
try {
const res = await fetch("http://localhost/api/promotions", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ idPromo: "NEW2025" }),
});
assertEquals(res.status, 400);
} finally {
restoreFetch();
}
});
Deno.test("mock API: PUT /promotions/:id updates promotion", async () => {
const updated = { idPromo: "4AFISE25/26", annee: "2026" };
mockFetch({
"/promotions/4AFISE25%2F26": { method: "PUT", status: 200, body: updated },
});
try {
const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({ annee: "2026" }),
});
assertEquals(res.status, 200);
const data: Promotion = await res.json();
assertEquals(data.annee, "2026");
} finally {
restoreFetch();
}
});
Deno.test("mock API: DELETE /promotions/:id returns 204", async () => {
mockFetch({ "/promotions/4AFISE25%2F26": { method: "DELETE", status: 204 } });
try {
const res = await fetch("http://localhost/api/promotions/4AFISE25%2F26", {
method: "DELETE",
});
assertEquals(res.status, 204);
} finally {
restoreFetch();
}
});
// --- Mock DB ---
Deno.test("mock DB: find promotion by idPromo", () => {
const db = createMockDb({ tables: { promotions: [...promotions] } });
const p = db.findOne<Promotion>(
"promotions",
(r) => r.idPromo === "4AFISE25/26",
);
assertExists(p);
assertEquals(p.annee, "2025");
});
Deno.test("mock DB: insert promotion", () => {
const db = createMockDb({ tables: { promotions: [...promotions] } });
db.insert<Promotion>("promotions", { idPromo: "NEW2025", annee: "2025" });
assertEquals(db.getTable("promotions").length, 4);
});
Deno.test("mock DB: update promotion annee", () => {
const db = createMockDb({ tables: { promotions: [...promotions] } });
db.updateWhere<Promotion>(
"promotions",
(p) => p.idPromo === "4AFISE25/26",
{ annee: "2026" },
);
assertEquals(
db.findOne<Promotion>("promotions", (p) => p.idPromo === "4AFISE25/26")
?.annee,
"2026",
);
});
Deno.test("mock DB: delete promotion", () => {
const db = createMockDb({ tables: { promotions: [...promotions] } });
const count = db.deleteWhere<Promotion>(
"promotions",
(p) => p.idPromo === "4AFISE25/26",
);
assertEquals(count, 1);
assertEquals(db.getTable("promotions").length, 2);
});
-216
View File
@@ -1,216 +0,0 @@
// #109 - Unit tests for /students endpoints
// Tests purs : fixtures, mock API, mock DB — aucun appel réseau réel
import { assertEquals, assertExists } from "@std/assert";
import { getFetchCalls, mockFetch, restoreFetch } from "../helpers/api_mock.ts";
import { createMockDb } from "../helpers/db_mock.ts";
import { type Student, students } from "../helpers/fixtures.ts";
// --- Fixtures ---
Deno.test("students: fixtures have correct shape", () => {
assertEquals(students.length, 3);
assertEquals(typeof students[0].numEtud, "number");
assertEquals(typeof students[0].nom, "string");
assertEquals(typeof students[0].prenom, "string");
assertEquals(typeof students[0].idPromo, "string");
});
Deno.test("students: two students belong to the same promo", () => {
const promo4 = students.filter((s) => s.idPromo === "4AFISE25/26");
assertEquals(promo4.length, 2);
});
// --- Mock API - GET /students ---
Deno.test("mock API: GET /students returns list", async () => {
mockFetch({ "/students": students });
try {
const res = await fetch("http://localhost/api/students");
assertEquals(res.status, 200);
const data: Student[] = await res.json();
assertEquals(data.length, 3);
assertExists(data.find((s) => s.nom === "Dupont"));
} finally {
restoreFetch();
}
});
Deno.test("mock API: GET /students?idPromo filters by promo", async () => {
const filtered = students.filter((s) => s.idPromo === "4AFISE25/26");
mockFetch({ "/students": filtered });
try {
const res = await fetch(
"http://localhost/api/students?idPromo=4AFISE25/26",
);
const data: Student[] = await res.json();
assertEquals(data.length, 2);
assertEquals(data.every((s) => s.idPromo === "4AFISE25/26"), true);
} finally {
restoreFetch();
}
});
Deno.test("mock API: GET /students/:numEtud returns one student", async () => {
mockFetch({ "/students/21212006": students[0] });
try {
const res = await fetch("http://localhost/api/students/21212006");
assertEquals(res.status, 200);
const data: Student = await res.json();
assertEquals(data.numEtud, 21212006);
assertEquals(data.nom, "Dupont");
} finally {
restoreFetch();
}
});
Deno.test("mock API: GET /students/:numEtud 404 when not found", async () => {
mockFetch({
"/students/99999": {
status: 404,
body: { error: "Ressource introuvable" },
},
});
try {
const res = await fetch("http://localhost/api/students/99999");
assertEquals(res.status, 404);
} finally {
restoreFetch();
}
});
Deno.test("mock API: POST /students creates student", 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({
nom: "Dupont",
prenom: "Jean",
idPromo: "4AFISE25/26",
}),
});
assertEquals(res.status, 201);
const data: Student = await res.json();
assertEquals(data.nom, "Dupont");
} finally {
restoreFetch();
}
});
Deno.test("mock API: PUT /students/:numEtud updates student", async () => {
const updated = { ...students[0], nom: "Dupont-Modifié" };
mockFetch({
"/students/21212006": { method: "PUT", status: 200, body: updated },
});
try {
const res = await fetch("http://localhost/api/students/21212006", {
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
nom: "Dupont-Modifié",
prenom: "Jean",
idPromo: "4AFISE25/26",
}),
});
assertEquals(res.status, 200);
const data: Student = await res.json();
assertEquals(data.nom, "Dupont-Modifié");
} finally {
restoreFetch();
}
});
Deno.test("mock API: DELETE /students/:numEtud returns 204", async () => {
mockFetch({ "/students/21212006": { method: "DELETE", status: 204 } });
try {
const res = await fetch("http://localhost/api/students/21212006", {
method: "DELETE",
});
assertEquals(res.status, 204);
} finally {
restoreFetch();
}
});
Deno.test("mock API: POST /students 400 on missing fields", async () => {
mockFetch({ "/students": { method: "POST", status: 400 } });
try {
const res = await fetch("http://localhost/api/students", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ nom: "Test" }),
});
assertEquals(res.status, 400);
} finally {
restoreFetch();
}
});
// --- Mock DB ---
Deno.test("mock DB: find student by numEtud", () => {
const db = createMockDb({ tables: { students: [...students] } });
const s = db.findOne<Student>("students", (r) => r.numEtud === 21212006);
assertExists(s);
assertEquals(s.nom, "Dupont");
});
Deno.test("mock DB: filter students by idPromo", () => {
const db = createMockDb({ tables: { students: [...students] } });
const rows = db.findMany<Student>(
"students",
(s) => s.idPromo === "4AFISE25/26",
);
assertEquals(rows.length, 2);
});
Deno.test("mock DB: insert student increments count", () => {
const db = createMockDb({ tables: { students: [...students] } });
db.insert<Student>("students", {
numEtud: 21212099,
nom: "Test",
prenom: "Ing",
idPromo: "4AFISE25/26",
});
assertEquals(db.getTable("students").length, 4);
});
Deno.test("mock DB: update student nom", () => {
const db = createMockDb({ tables: { students: [...students] } });
const count = db.updateWhere<Student>(
"students",
(s) => s.numEtud === 21212006,
{ nom: "Nouveau" },
);
assertEquals(count, 1);
assertEquals(
db.findOne<Student>("students", (s) => s.numEtud === 21212006)?.nom,
"Nouveau",
);
});
Deno.test("mock DB: delete student removes exactly one", () => {
const db = createMockDb({ tables: { students: [...students] } });
const count = db.deleteWhere<Student>(
"students",
(s) => s.numEtud === 21212006,
);
assertEquals(count, 1);
assertEquals(db.getTable("students").length, 2);
});
Deno.test("mock API: getFetchCalls tracks student requests", async () => {
mockFetch({ "/students": students });
try {
await fetch("http://localhost/api/students");
await fetch("http://localhost/api/students?idPromo=4AFISE25/26");
const calls = getFetchCalls();
assertEquals(calls.length, 2);
assertEquals(calls[0].method, "GET");
} finally {
restoreFetch();
}
});
-33
View File
@@ -1,33 +0,0 @@
#!/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