Files
datacat/documentation/architecture.md
z3n 921a6e652b feat: initial scaffold — MVP catalogue de données d'API
- Backend NestJS : CRUD api_entries + categories, upload YAML multi-fichiers,
  génération docs AsyncAPI (@asyncapi/cli@6.0.0) et OpenAPI (redocly)
- Fix: route wildcard GET /api/apis/:id/*path pour servir les assets statiques
  (CSS/JS) générés par AsyncAPI HTML template (contournement bug path-to-regexp v8)
- Frontend Angular 19 : pages catalog, browse, api-detail, doc-viewer, upload (DSFR)
- Seeder de données de démo (idempotent)
- Docker Compose dev + Dockerfiles + manifests K8s
- Documentation : README, architecture, référence API REST

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-05-22 08:42:54 +00:00

166 lines
6.1 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(),
title VARCHAR(255) NOT NULL,
version VARCHAR(50) NOT NULL,
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),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
### 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 désassocie automatiquement ses APIs et sous-catégories enfants (logique applicative).
---
## 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.