Files
datacat/documentation/architecture.md
z3n 0976c33059 feat(api-detail): isCurrent + setCurrent + màj doc et seeder
- Shared types : ajout isCurrent sur ApiEntry et ApiEntryListItem
- Backend entity : colonne is_current (boolean, default true)
- Backend service : isCurrent calculé à la création, setCurrent()
  marque une version comme courante (siblings → false)
- Backend controller : POST /api/apis/:id/set-current
- Frontend ApiService : méthode setCurrent()
- Browse/upload : affichage badge "Courante" via isCurrent
- Seeder : données de démo mises à jour
- Documentation : api-reference, architecture, local-setup, CLAUDE.md

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-06-19 14:23:31 +00:00

181 lines
7.2 KiB
Markdown

# Architecture — Datacat
## Vue d'ensemble
Datacat est un catalogue de données d'API organisé en monorepo pnpm :
```
datacat/
├── back/ # API NestJS + TypeORM + PostgreSQL (port 3000 interne, 3010 hôte)
├── front-public/ # Frontend Angular 19 avec DSFR (port 4200)
├── shared/ # Types TypeScript partagés (@datacat/shared)
└── infra/ # Docker Compose, Dockerfiles, Kubernetes
```
---
## Flux principal
```
Utilisateur upload YAML
POST /api/uploads (multipart, 1 à 20 fichiers)
├─ 1 fichier → lecture directe
└─ N fichiers → détection du fichier principal + bundling (redocly bundle / asyncapi bundle)
ApisService.create()
→ ApiEntry en base (status: PENDING)
▼ (fire-and-forget)
DocsGenerationService.generate(id)
├─ type ASYNCAPI → asyncapi generate fromTemplate input.yaml @asyncapi/html-template
└─ type OPENAPI → redocly build-docs input.yaml --output index.html
ApiEntry mis à jour
→ status: GENERATED + htmlPath: /app/docs-output/<uuid>/index.html
OU status: ERROR + errorMessage: <raison>
GET /api/apis/:id/docs → res.sendFile(htmlPath)
```
---
## Services NestJS
### `ApisService` (`back/src/modules/apis/apis.service.ts`)
CRUD sur la table `api_entries`. Expose deux méthodes de mise à jour :
- `update(id, dto)` — pour le controller (champs éditables par l'utilisateur)
- `updateInternal(id, fields)` — pour `DocsGenerationService` (status, htmlPath, errorMessage)
### `UploadsController` (`back/src/modules/uploads/uploads.controller.ts`)
Gère l'upload multipart via `FilesInterceptor` (jusqu'à 20 fichiers).
- Détecte automatiquement le type (`openapi:` ou `asyncapi:` en début de fichier)
- Si plusieurs fichiers : détecte le fichier principal et bundle les autres via la CLI correspondante
- Lance `DocsGenerationService.generate()` en fire-and-forget après création
### `DocsGenerationService` (`back/src/modules/docs-generation/docs-generation.service.ts`)
Génère la documentation HTML à partir du YAML stocké en base :
- **AsyncAPI** : `asyncapi generate fromTemplate <yaml> @asyncapi/html-template --output <dir> --no-interactive --install --force-write`
- **OpenAPI** : `redocly build-docs <yaml> --output <dir>/index.html`
- Sortie : `$DOCS_OUTPUT_DIR/<uuid>/index.html` (défaut : `/app/docs-output`)
### `CategoriesService` (`back/src/modules/categories/categories.service.ts`)
CRUD sur la table `categories` + endpoint `browse` qui retourne :
- La catégorie courante
- Le fil d'Ariane (breadcrumb)
- Les sous-catégories directes avec comptages
- Les APIs directement dans la catégorie
### `SeederService` (`back/src/modules/seeder/seeder.service.ts`)
Injecte des données de démonstration au démarrage (idempotent : ne re-seed que si la table est vide).
---
## Schéma de base de données
### Table `api_entries`
```sql
CREATE TABLE api_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
convention VARCHAR(30) NOT NULL DEFAULT 'CONSULTER'
CHECK (convention IN ('CONSULTER','ENREGISTRER','ETRE_NOTIFIE')),
name VARCHAR(255) NOT NULL DEFAULT '',
provider VARCHAR(255) NOT NULL DEFAULT '',
version_major INT NOT NULL DEFAULT 0,
version_minor INT NOT NULL DEFAULT 0,
version_patch INT NOT NULL DEFAULT 0,
description TEXT,
type VARCHAR(10) NOT NULL CHECK (type IN ('ASYNCAPI', 'OPENAPI')),
yaml_content TEXT NOT NULL,
html_path VARCHAR(500),
status VARCHAR(10) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING', 'GENERATED', 'ERROR')),
error_message TEXT,
category_id VARCHAR(36),
contact_functional_name VARCHAR(255) NOT NULL DEFAULT '',
contact_functional_entity VARCHAR(255) NOT NULL DEFAULT '',
contact_functional_email VARCHAR(255) NOT NULL DEFAULT '',
contact_technical_name VARCHAR(255) NOT NULL DEFAULT '',
contact_technical_entity VARCHAR(255) NOT NULL DEFAULT '',
contact_technical_email VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
> `title` et `version` sont des champs calculés (pas en DB) :
> - `title` = `CONVENTION_LABEL + ' ' + name` (ex: `"Consulter Petstore"`)
> - `version` = `"major.minor.patch"` (ex: `"1.0.0"`)
### Table `categories`
```sql
CREATE TABLE categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
parent_id VARCHAR(36), -- auto-référence (pas de FK TypeORM)
order_index INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
> Les relations `category_id` / `parent_id` sont des colonnes varchar sans contrainte de clé étrangère TypeORM.
> La suppression d'une catégorie est **rejetée avec 400** si le dossier contient des APIs ou des sous-dossiers (vérification applicative avant `DELETE`).
---
## Dépendances externes
| Outil | Version | Usage |
|-------|---------|-------|
| `@asyncapi/cli` | `6.0.0` | Génération HTML AsyncAPI (bundles generator 3.x) |
| `@redocly/cli` | latest | Génération HTML OpenAPI + bundling multi-fichiers |
Les deux CLIs sont installés dans les images Docker (`infra/Dockerfile.back.dev` pour le développement).
> **Important** : utiliser `@asyncapi/cli@6.0.0` — la version 4.x ne supporte qu'AsyncAPI spec ≤ 2.6.0.
> Le template `@asyncapi/html-template` est pré-installé dans l'image pour éviter les téléchargements au premier démarrage.
---
## Frontend Angular 19
Structure des pages (lazy loading via `loadComponent`) :
| Route | Composant | Description |
|-------|-----------|-------------|
| `/` | redirect → `/browse` | |
| `/browse` | `BrowseComponent` | Navigation par catégories (racine) |
| `/browse/:id` | `BrowseComponent` | Navigation dans une catégorie |
| `/catalog` | `CatalogComponent` | Liste toutes les APIs avec recherche/filtre |
| `/catalog/:id` | `ApiDetailComponent` | Détail d'une API (métadonnées + actions) |
| `/catalog/:id/docs` | `DocViewerComponent` | Iframe plein écran vers la doc générée |
| `/upload` | `UploadComponent` | Stepper 3 étapes pour importer un YAML |
**Conventions** : standalone components, signals (`signal()`, `computed()`), `inject()`, `ChangeDetectionStrategy.OnPush`.
---
## Infrastructure Docker (développement)
```
docker compose -f infra/docker-compose.yml up -d
```
| Service | Image | Port interne | Port hôte |
|---------|-------|-------------|-----------|
| `postgres` | `postgres:17` | 5432 | 5432 |
| `backend` | `Dockerfile.back.dev` | 3000 | 3010 |
| `frontend` | `Dockerfile.front.dev` | 4200 | 4200 |
> Le backend est exposé sur **3010** (pas 3000) car Gitea tourne sur 3000 sur la machine hôte.