- 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>
166 lines
6.1 KiB
Markdown
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.
|