Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ Plugins use TS module augmentation to extend `ProjectStore` and `Config` interfa
## Database (Prisma)

Multi-file schema in `apps/server/src/prisma/schema/*.prisma` (project, user, token, admin, topography).
Singleton PrismaClient in `apps/server/src/prisma.ts`. Queries centralized per resource, re-exported via `queries-index.ts`.
Migrations: standard Prisma Migrate. Major version data migrations in `migrations/v9/`.

## Environment config
Expand Down
8 changes: 8 additions & 0 deletions apps/server-nestjs/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ KEYCLOAK_PROTOCOL=http
KEYCLOAK_CLIENT_ID=dso-console-backend
# Secret du client Keycloak backend (confidentiel)
KEYCLOAK_CLIENT_SECRET=client-secret-backend
# Identifiant de l'administrateur Keycloak
KEYCLOAK_ADMIN=admin
# Mot de passe de l'administrateur Keycloak
KEYCLOAK_ADMIN_PASSWORD=admin
# Identifiant administrateur Keycloak (utilisé pour l'API admin)
KEYCLOAK_ADMIN=admin
# Mot de passe administrateur Keycloak (confidentiel)
KEYCLOAK_ADMIN_PASSWORD=admin
# URL de redirection après authentification Keycloak
KEYCLOAK_REDIRECT_URI=http://localhost:8080
# Port d'écoute du serveur backend
Expand Down
4 changes: 4 additions & 0 deletions apps/server-nestjs/.env.docker-example
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ KEYCLOAK_PROTOCOL=http
KEYCLOAK_CLIENT_ID=dso-console-backend
# Secret du client Keycloak backend (confidentiel)
KEYCLOAK_CLIENT_SECRET=client-secret-backend
# Identifiant de l'administrateur Keycloak
KEYCLOAK_ADMIN=admin
# Mot de passe de l'administrateur Keycloak
KEYCLOAK_ADMIN_PASSWORD=admin
# URL de redirection après authentification Keycloak
KEYCLOAK_REDIRECT_URI=http://localhost:8080
# Port d'écoute du serveur dans le réseau Docker
Expand Down
4 changes: 4 additions & 0 deletions apps/server-nestjs/.env.integ-example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ KEYCLOAK_CLIENT_SECRET=
KEYCLOAK_DOMAIN=
# Royaume Keycloak d'intégration
KEYCLOAK_REALM=
# Identifiant de l'administrateur Keycloak
KEYCLOAK_ADMIN=
# Mot de passe de l'administrateur Keycloak
KEYCLOAK_ADMIN_PASSWORD=

# --- ArgoCD ---
# Namespace Kubernetes dans lequel ArgoCD est déployé
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Architecture d’un module (pattern `apps/server-nestjs/src/modules/*`)

Les modules NestJS métier vivent dans `src/modules/<nom-du-module>/` et suivent un découpage “vertical slice” avec des responsabilités explicites : **client**, **service (API publique)**, **controller service (orchestration)**, **datastore**, **utils** et **tests**.

Exemples concrets :
- Module GitLab : `src/modules/gitlab/`
- Module Keycloak : `src/modules/keycloak/`

## Structure type

```txt
src/modules/<module>/
├── <module>.module.ts
├── <module>.constants.ts
├── <module>-client.service.ts
├── <module>.service.ts
├── <module>-controller.service.ts
├── <module>-datastore.service.ts
├── <module>.utils.ts
├── <module>-testing.utils.ts
└── *.spec.ts
```

## Sens des dépendances (flow recommandé)

Objectif : un flux de dépendances lisible et sans cycles.

```txt
<module>-controller.service.ts
↙ ↘
<module>-client.service.ts <module>-datastore.service.ts

<module>.service.ts (API publique)
↙ ↘
<module>-client.service.ts <module>-datastore.service.ts
```

Règles pratiques :
- Le `<module>.service.ts` est l’API publique du module (provider exporté) : les autres modules importent ce service, pas le controller.
- Le `controller service` est un entrypoint interne (cron, events, reconcile) et orchestre en appelant directement le `client` (et le `datastore` si nécessaire), sans dépendre du `service`.
- Le `service` contient les règles métier (décisions, transformations, validations) et s’appuie sur le `client` (et le `datastore` quand la lecture/écriture DB fait partie du cas d’usage).
- Le `client` encapsule l’accès à une API externe (initialisation + appels + erreurs bas niveau).
- Le `datastore` encapsule l’accès DB (Prisma) et expose des méthodes de lecture/écriture typées.
- Les `utils` restent “purs” (pas d’IO, pas d’injection Nest).
- Les `testing utils` centralisent les factories/fixtures pour réduire la duplication dans les tests.

## Composants

### `<module>.module.ts`

Rôle :
- Déclare les providers, imports, exports du module.
- Exporte le `service` du module (`<module>.service.ts`) qui constitue l’API publique.

### `<module>-client.service.ts`

Rôle :
- Adapter vers le système externe (SDK HTTP, client Keycloak, client GitLab, etc.).
- Conserver un contrat stable pour le reste du module.
- Mapper/normaliser les erreurs externes si nécessaire.

À éviter :
- Décisions métier (permissions, synchronisation, règles de purge) : elles vont dans `<module>.service.ts` ou le controller service.

### `<module>.service.ts`

Rôle :
- API publique du module.
- Coeur métier du module (logique, mapping, validations, règles).
- Appels aux dépendances (client et datastore) via des méthodes orientées domaine.

À éviter :
- Cron/events : c’est le rôle du controller service.

### `<module>-controller.service.ts`

Rôle :
- Orchestrateur de workflows : `@Cron`, `@OnEvent`, reconcile périodique, tâches “batch”.
- Coordination entre `client` et `datastore` (sans dépendre du `service`).
- Garde-fous “safety” avant opérations destructrices (ex: suppression de groupes orphelins).

### `<module>-datastore.service.ts`

Rôle :
- Accès DB via Prisma (select/include, transactions, pagination).
- Exposition de types agrégés utiles au domaine (ex: `ProjectWithDetails`).

À éviter :
- Appliquer des règles métier (ex: calcul de permissions) : on garde le datastore centré persistence.

### `<module>.utils.ts`

Rôle :
- Fonctions utilitaires pures : mapping, helpers de collections, types partagés.
- Aucune dépendance Nest, aucune lecture/écriture DB, aucun appel réseau.

### `<module>-testing.utils.ts`

Rôle :
- Factories typées pour les structures fréquemment utilisées en tests.
- Support d’`overrides` pour construire rapidement des variantes.
- Centralisation des erreurs/fake responses spécifiques au module (quand utile).

## Tests (Vitest)

### `<module>.service.spec.ts`

Cible :
- Logique métier : transformations, décisions, mapping d’erreurs.

Approche :
- Mock du `client` et du `datastore`.
- Pas d’IO réel.

### `<module>-controller.service.spec.ts`

Cible :
- Orchestration : séquences d’appels, side-effects attendus, reconcile.

Approche :
- Mock du `service`, du `datastore`, et des appels externes.
- Vérification des appels effectués et des paramètres attendus.

### `<module>-datastore.service.spec.ts` (si présent)

Cible :
- Forme des requêtes Prisma, mapping de résultat, typage de l’agrégat renvoyé.

Approche :
- Mock de Prisma/DatabaseService, pas de logique métier.
21 changes: 18 additions & 3 deletions apps/server-nestjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@
"db:migrate": "prisma migrate dev --name dso",
"db:reset": "prisma migrate reset",
"format": "eslint ./ --fix",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"prestart": "prisma generate",
"start": "nest start",
"start:debug": "nest start --debug --watch",
"start:dev": "nest start --watch",
"start:prod": "node dist/main"
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"pretest": "prisma generate",
"test": "vitest run",
"test:watch": "vitest",
"pretest:cov": "prisma generate",
"test:cov": "vitest run --coverage",
"test:debug": "vitest --inspect"
},
"dependencies": {
"@cpn-console/argocd-plugin": "workspace:^",
Expand All @@ -38,11 +45,14 @@
"@fastify/swagger-ui": "^4.2.0",
"@gitbeaker/core": "^40.6.0",
"@gitbeaker/rest": "^40.6.0",
"@keycloak/keycloak-admin-client": "^24.0.0",
"@kubernetes-models/argo-cd": "^2.7.2",
"@nestjs/common": "^11.1.16",
"@nestjs/config": "^4.0.3",
"@nestjs/core": "^11.1.16",
"@nestjs/event-emitter": "^3.0.1",
"@nestjs/platform-express": "^11.1.16",
"@nestjs/schedule": "^6.1.1",
"@nestjs/terminus": "^11.1.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/auto-instrumentations-node": "^0.70.1",
Expand All @@ -63,14 +73,18 @@
"fastify": "^4.29.1",
"fastify-keycloak-adapter": "2.3.2",
"json-2-csv": "^5.5.10",
"keycloak-connect": "^25.0.0",
"mustache": "^4.2.0",
"nest-keycloak-connect": "^1.10.1",
"nestjs-pino": "^4.6.0",
"pino-http": "^11.0.0",
"prisma": "^6.19.2",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"undici": "^7.22.0",
"vitest-mock-extended": "^2.0.2"
"vitest-mock-extended": "^2.0.2",
"yaml": "^2.8.2",
"zod": "^3.25.76"
},
"devDependencies": {
"@cpn-console/eslint-config": "workspace:^",
Expand All @@ -89,6 +103,7 @@
"eslint": "^9.39.4",
"fastify-plugin": "^5.1.0",
"globals": "^16.5.0",
"msw": "^2.12.10",
"nodemon": "^3.1.14",
"pino-pretty": "^13.1.3",
"rimraf": "^6.1.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export class ConfigurationService {
keycloakRealm = process.env.KEYCLOAK_REALM
keycloakClientId = process.env.KEYCLOAK_CLIENT_ID
keycloakClientSecret = process.env.KEYCLOAK_CLIENT_SECRET
keycloakAdmin = process.env.KEYCLOAK_ADMIN
keycloakAdminPassword = process.env.KEYCLOAK_ADMIN_PASSWORD
keycloakRedirectUri = process.env.KEYCLOAK_REDIRECT_URI

adminsUserId = process.env.ADMIN_KC_USER_ID
? process.env.ADMIN_KC_USER_ID.split(',')
: []
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Controller, Get, Inject } from '@nestjs/common'
import { HealthCheck, HealthCheckService } from '@nestjs/terminus'
import { KeycloakHealthService } from '../../../modules/keycloak/keycloak-health.service'
import { DatabaseHealthService } from '../database/database-health.service'

@Controller('health')
export class HealthController {
constructor(
@Inject(HealthCheckService) private readonly health: HealthCheckService,
@Inject(DatabaseHealthService) private readonly database: DatabaseHealthService,
@Inject(KeycloakHealthService) private readonly keycloak: KeycloakHealthService,
) {}

@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.database.check('database'),
() => this.keycloak.check('keycloak'),
])
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Module } from '@nestjs/common'
import { TerminusModule } from '@nestjs/terminus'
import { KeycloakModule } from '../../../modules/keycloak/keycloak.module'
import { DatabaseHealthService } from '../database/database-health.service'
import { HealthController } from './health.controller'

@Module({
imports: [
TerminusModule,
DatabaseHealthService,
KeycloakModule,
],
controllers: [HealthController],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { Module } from '@nestjs/common'

import { ConfigurationModule } from './configuration/configuration.module'
import { DatabaseService } from './database/database.service'
import { PrismaService } from './database/prisma.service'
import { HealthModule } from './health/health.module'
import { HttpClientService } from './http-client/http-client.service'
import { LoggerModule } from './logger/logger.module'
import { ServerService } from './server/server.service'
import { TelemetryModule } from './telemetry/telemetry.module'

@Module({
providers: [DatabaseService, HttpClientService, ServerService],
providers: [DatabaseService, PrismaService, HttpClientService, ServerService],
imports: [LoggerModule, ConfigurationModule, TelemetryModule, HealthModule],
exports: [DatabaseService, HttpClientService, ServerService],
exports: [DatabaseService, PrismaService, HttpClientService, ServerService],
})
export class InfrastructureModule {}
10 changes: 9 additions & 1 deletion apps/server-nestjs/src/main.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { Module } from '@nestjs/common'
import { EventEmitterModule } from '@nestjs/event-emitter'
import { ScheduleModule } from '@nestjs/schedule'

import { CpinModule } from './cpin-module/cpin.module'
import { KeycloakModule } from './modules/keycloak/keycloak.module'

// This module only exists to import other module.
// « One module to rule them all, and in NestJs bind them »
@Module({
imports: [CpinModule],
imports: [
CpinModule,
KeycloakModule,
EventEmitterModule.forRoot(),
ScheduleModule.forRoot(),
],
controllers: [],
providers: [],
})
Expand Down
Loading
Loading