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,127 @@
# 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**, **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>.service.ts
↙ ↘
<module>-client <module>-datastore
.service.ts .service.ts
```

Règles pratiques :
- Le `controller service` orchestre des workflows (cron, events, reconcile) et appelle le `service` et/ou le `datastore`.
- Le `service` contient les règles métier (décisions, transformations, validations) et s’appuie sur le `client` et le `datastore`.
- 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 généralement le service principal du module.

### `<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 :
- Coeur métier du module (logique, mapping, validations, règles).
- Appels aux dépendances (client/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 datastore et service (et parfois appels directs au client pour des opérations transverses).
- 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.
22 changes: 19 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,15 @@
"@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",
"@opentelemetry/api": "^1.9.0",
"@prisma/client": "^6.19.2",
"@ts-rest/core": "^3.52.1",
"@ts-rest/fastify": "^3.52.1",
Expand All @@ -53,14 +64,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 @@ -79,6 +94,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 @@ -7,6 +7,7 @@ import { ConfigurationModule } from '@/cpin-module/infrastructure/configuration/
import { PluginManagementService } from '../plugin-management/plugin-management.service'
import { DatabaseInitializationService } from '../database-initialization/database-initialization.service'
import { DatabaseService } from '@/cpin-module/infrastructure/database/database.service'
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'

describe('applicationInitializationServiceService', () => {
let service: ApplicationInitializationService
Expand All @@ -19,6 +20,10 @@ describe('applicationInitializationServiceService', () => {
PluginManagementService,
DatabaseInitializationService,
DatabaseService,
{
provide: PrismaService,
useValue: {},
},
],
}).compile()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ import { Test } from '@nestjs/testing'
import { describe, beforeEach, it, expect } from 'vitest'

import { DatabaseInitializationService } from './database-initialization.service'
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'

describe('databaseInitializationService', () => {
let service: DatabaseInitializationService

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseInitializationService],
providers: [
DatabaseInitializationService,
{
provide: PrismaService,
useValue: {},
},
],
}).compile()

service = module.get<DatabaseInitializationService>(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import prisma from '@/prisma'
import { Injectable, Logger } from '@nestjs/common'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { PrismaService } from '@/cpin-module/infrastructure/database/prisma.service'

import { modelKeys } from './utils'

type ExtractKeysWithFields<T> = {
[K in keyof T]: T[K] extends { fields: any } ? K : never;
}[keyof T]

type Models = ExtractKeysWithFields<typeof prisma>

type Imports = Partial<Record<Models, object[]>> & {
associations: [Models, any[]]
type ModelKey = (typeof modelKeys)[number]
type Imports = Partial<Record<ModelKey, object[]>> & {
associations: [ModelKey, any[]][]
}

@Injectable()
Expand All @@ -19,6 +14,8 @@ export class DatabaseInitializationService {
DatabaseInitializationService.name,
)

constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {}

async initDb(data: Imports) {
const dataStringified = JSON.stringify(data)
const dataParsed = JSON.parse(dataStringified, (key, value) => {
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,17 +1,27 @@
import type { TestingModule } from '@nestjs/testing'
import { Test } from '@nestjs/testing'
import { describe, beforeEach, it, expect } from 'vitest'
import { describe, beforeEach, it, expect, vi } from 'vitest'

import { DatabaseService } from './database.service'
import { ConfigurationModule } from '../configuration/configuration.module'
import { PrismaService } from './prisma.service'

describe('databaseService', () => {
let service: DatabaseService

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [ConfigurationModule],
providers: [DatabaseService],
providers: [
DatabaseService,
{
provide: PrismaService,
useValue: {
$connect: vi.fn().mockResolvedValue(undefined),
$disconnect: vi.fn().mockResolvedValue(undefined),
},
},
],
}).compile()

service = module.get<DatabaseService>(DatabaseService)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import prisma from '@/prisma'
import { Inject, Injectable, Logger } from '@nestjs/common'
import { setTimeout } from 'node:timers/promises'
import { ConfigurationService } from '../configuration/configuration.service'
import { PrismaService } from './prisma.service'

@Injectable()
export class DatabaseService {
private readonly loggerService = new Logger(DatabaseService.name)

constructor(@Inject(ConfigurationService) private readonly configurationService: ConfigurationService) {
constructor(
@Inject(PrismaService) private readonly prisma: PrismaService,
@Inject(ConfigurationService) private readonly configurationService: ConfigurationService,
) {
this.DELAY_BEFORE_RETRY
= this.configurationService.isTest || this.configurationService.isCI
? 1000
Expand All @@ -33,7 +36,7 @@ export class DatabaseService {
`Trying to connect to Postgres with: ${this.configurationService.dbUrl}`,
)
}
await prisma.$connect()
await this.prisma.$connect()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should let the prisma service handle it


this.loggerService.log('Connected to Postgres!')
} catch (error) {
Expand All @@ -59,7 +62,7 @@ export class DatabaseService {
async closeConnections() {
this.closingConnections = true
try {
await prisma.$disconnect()
await this.prisma.$disconnect()
} catch (error) {
this.loggerService.error(error)
} finally {
Expand Down
Loading
Loading