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>
This commit is contained in:
z3n
2026-05-22 08:42:54 +00:00
commit 921a6e652b
87 changed files with 16719 additions and 0 deletions

View File

@@ -0,0 +1,730 @@
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApiEntryEntity } from '../apis/api-entry.entity';
import { CategoryEntity } from '../categories/category.entity';
import { DocsGenerationService } from '../docs-generation/docs-generation.service';
/* ─── YAML demo data ─────────────────────────────────────────────────────── */
const PETSTORE_YAML = `openapi: "3.0.0"
info:
title: Swagger Petstore
description: A sample API that uses a petstore as an example to demonstrate features in the OpenAPI specification.
version: "3.0.4"
license:
name: Apache 2.0
servers:
- url: https://petstore.swagger.io/v3
paths:
/pets:
get:
summary: List all pets
operationId: listPets
tags: [pets]
parameters:
- name: limit
in: query
description: How many items to return at one time (max 100)
required: false
schema:
type: integer
format: int32
maximum: 100
responses:
"200":
description: A list of pets
content:
application/json:
schema:
$ref: "#/components/schemas/Pets"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
post:
summary: Create a pet
operationId: createPets
tags: [pets]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/NewPet"
responses:
"201":
description: Null response
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
/pets/{petId}:
get:
summary: Info for a specific pet
operationId: showPetById
tags: [pets]
parameters:
- name: petId
in: path
required: true
description: The id of the pet to retrieve
schema:
type: string
responses:
"200":
description: Expected response to a valid request
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
default:
description: unexpected error
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
components:
schemas:
Pet:
type: object
required: [id, name]
properties:
id:
type: integer
format: int64
name:
type: string
tag:
type: string
NewPet:
type: object
required: [name]
properties:
name:
type: string
tag:
type: string
Pets:
type: array
maxItems: 100
items:
$ref: "#/components/schemas/Pet"
Error:
type: object
required: [code, message]
properties:
code:
type: integer
format: int32
message:
type: string
`;
const STREETLIGHTS_YAML = `asyncapi: "3.0.0"
info:
title: Streetlights Kafka API
version: "1.0.0"
description: The Smartylighting Streetlights API allows you to remotely manage the city lights.
license:
name: Apache 2.0
servers:
scram-connections:
host: test.mykafkacluster.org:18092
protocol: kafka-secure
description: Test broker (SCRAM-SHA-512 security)
channels:
lightingMeasured:
address: "smartylighting.streetlights.1.0.event.{streetlightId}.lighting.measured"
messages:
lightMeasured:
$ref: "#/components/messages/LightMeasured"
parameters:
streetlightId:
$ref: "#/components/parameters/streetlightId"
lightTurnOn:
address: "smartylighting.streetlights.1.0.action.{streetlightId}.turn.on"
messages:
turnOn:
$ref: "#/components/messages/TurnOn"
parameters:
streetlightId:
$ref: "#/components/parameters/streetlightId"
lightTurnOff:
address: "smartylighting.streetlights.1.0.action.{streetlightId}.turn.off"
messages:
turnOff:
$ref: "#/components/messages/TurnOff"
parameters:
streetlightId:
$ref: "#/components/parameters/streetlightId"
operations:
receiveLightMeasurement:
action: receive
channel:
$ref: "#/channels/lightingMeasured"
messages:
- $ref: "#/channels/lightingMeasured/messages/lightMeasured"
turnOn:
action: send
channel:
$ref: "#/channels/lightTurnOn"
messages:
- $ref: "#/channels/lightTurnOn/messages/turnOn"
turnOff:
action: send
channel:
$ref: "#/channels/lightTurnOff"
messages:
- $ref: "#/channels/lightTurnOff/messages/turnOff"
components:
messages:
LightMeasured:
name: LightMeasured
title: Light measured
summary: Inform about environmental lighting conditions.
payload:
$ref: "#/components/schemas/LightMeasuredPayload"
TurnOn:
name: TurnOn
title: Turn on
summary: Command a specific streetlight to turn on.
payload:
$ref: "#/components/schemas/TurnOnPayload"
TurnOff:
name: TurnOff
title: Turn off
summary: Command a specific streetlight to turn off.
payload:
$ref: "#/components/schemas/TurnOffPayload"
parameters:
streetlightId:
description: The ID of the streetlight.
schemas:
LightMeasuredPayload:
type: object
properties:
id:
type: integer
minimum: 0
description: ID of the streetlight.
lumens:
type: integer
minimum: 0
description: Light intensity measured in lumens.
sentAt:
type: string
format: date-time
TurnOnPayload:
type: object
properties:
command:
type: string
enum: [on]
sentAt:
type: string
format: date-time
TurnOffPayload:
type: object
properties:
command:
type: string
enum: [off]
sentAt:
type: string
format: date-time
`;
const ACCOUNT_SERVICE_YAML = `asyncapi: "3.0.0"
info:
title: Account Service
version: "1.0.0"
description: This service manages user accounts and publishes events on user lifecycle changes.
channels:
userSignedUp:
address: user/signedup
messages:
userSignedUp:
$ref: "#/components/messages/UserSignedUp"
userPasswordChanged:
address: user/passwordchanged
messages:
userPasswordChanged:
$ref: "#/components/messages/UserPasswordChanged"
operations:
sendUserSignedUp:
action: send
channel:
$ref: "#/channels/userSignedUp"
messages:
- $ref: "#/channels/userSignedUp/messages/userSignedUp"
sendUserPasswordChanged:
action: send
channel:
$ref: "#/channels/userPasswordChanged"
messages:
- $ref: "#/channels/userPasswordChanged/messages/userPasswordChanged"
components:
messages:
UserSignedUp:
payload:
$ref: "#/components/schemas/UserSignedUpPayload"
UserPasswordChanged:
payload:
$ref: "#/components/schemas/UserPasswordChangedPayload"
schemas:
UserSignedUpPayload:
type: object
properties:
displayName:
type: string
description: Name of the user
email:
type: string
format: email
description: Email of the user
createdAt:
type: string
format: date-time
UserPasswordChangedPayload:
type: object
properties:
userId:
type: string
changedAt:
type: string
format: date-time
`;
const ORDERS_YAML = `openapi: "3.0.0"
info:
title: Orders API
version: "1.0.0"
description: API de gestion des commandes clients.
servers:
- url: https://api.example.com/v1
paths:
/orders:
get:
summary: Lister les commandes
operationId: listOrders
tags: [orders]
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
"200":
description: Liste des commandes
content:
application/json:
schema:
$ref: "#/components/schemas/OrderList"
post:
summary: Créer une commande
operationId: createOrder
tags: [orders]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateOrder"
responses:
"201":
description: Commande créée
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
/orders/{orderId}:
get:
summary: Obtenir une commande
operationId: getOrder
tags: [orders]
parameters:
- name: orderId
in: path
required: true
schema:
type: string
responses:
"200":
description: Détails de la commande
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
description: Commande non trouvée
patch:
summary: Mettre à jour le statut
operationId: updateOrderStatus
tags: [orders]
parameters:
- name: orderId
in: path
required: true
schema:
type: string
requestBody:
content:
application/json:
schema:
type: object
properties:
status:
type: string
enum: [confirmed, shipped, delivered, cancelled]
responses:
"200":
description: Commande mise à jour
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
components:
schemas:
Order:
type: object
properties:
id:
type: string
customerId:
type: string
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
items:
type: array
items:
$ref: "#/components/schemas/OrderItem"
totalAmount:
type: number
format: float
createdAt:
type: string
format: date-time
OrderItem:
type: object
properties:
productId:
type: string
quantity:
type: integer
minimum: 1
price:
type: number
format: float
CreateOrder:
type: object
required: [customerId, items]
properties:
customerId:
type: string
items:
type: array
minItems: 1
items:
$ref: "#/components/schemas/OrderItem"
OrderList:
type: object
properties:
items:
type: array
items:
$ref: "#/components/schemas/Order"
total:
type: integer
page:
type: integer
limit:
type: integer
`;
const AUTH_YAML = `openapi: "3.0.0"
info:
title: Auth API
version: "1.0.0"
description: Service d'authentification et d'autorisation avec JWT.
servers:
- url: https://auth.example.com/v1
paths:
/auth/login:
post:
summary: Connexion
operationId: login
tags: [auth]
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/LoginRequest"
responses:
"200":
description: Authentification réussie
content:
application/json:
schema:
$ref: "#/components/schemas/TokenResponse"
"401":
description: Identifiants invalides
/auth/refresh:
post:
summary: Rafraîchir le token
operationId: refreshToken
tags: [auth]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [refreshToken]
properties:
refreshToken:
type: string
responses:
"200":
description: Nouveau token émis
content:
application/json:
schema:
$ref: "#/components/schemas/TokenResponse"
"401":
description: Refresh token invalide ou expiré
/auth/logout:
post:
summary: Déconnexion
operationId: logout
tags: [auth]
security:
- bearerAuth: []
responses:
"204":
description: Déconnecté avec succès
/users/me:
get:
summary: Profil utilisateur courant
operationId: getCurrentUser
tags: [users]
security:
- bearerAuth: []
responses:
"200":
description: Profil de l'utilisateur connecté
content:
application/json:
schema:
$ref: "#/components/schemas/User"
"401":
description: Non authentifié
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
schemas:
LoginRequest:
type: object
required: [email, password]
properties:
email:
type: string
format: email
password:
type: string
format: password
TokenResponse:
type: object
properties:
accessToken:
type: string
refreshToken:
type: string
expiresIn:
type: integer
description: Durée de validité en secondes
User:
type: object
properties:
id:
type: string
email:
type: string
format: email
displayName:
type: string
roles:
type: array
items:
type: string
createdAt:
type: string
format: date-time
`;
/* ─── Seeder ──────────────────────────────────────────────────────────────── */
@Injectable()
export class SeederService implements OnApplicationBootstrap {
constructor(
@InjectRepository(ApiEntryEntity)
private readonly apiRepo: Repository<ApiEntryEntity>,
@InjectRepository(CategoryEntity)
private readonly categoryRepo: Repository<CategoryEntity>,
private readonly docsService: DocsGenerationService,
) {}
async onApplicationBootstrap(): Promise<void> {
const count = await this.apiRepo.count();
if (count > 0) return; /* Idempotent — ne ré-exécute pas si des données existent */
await this.seed();
}
private async seed(): Promise<void> {
/* Catégories racines */
const ecommerce = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'E-commerce',
description: 'APIs liées au commerce électronique',
parentId: null,
orderIndex: 0,
}),
);
const infrastructure = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Infrastructure',
description: "APIs d'infrastructure et événements système",
parentId: null,
orderIndex: 1,
}),
);
const identite = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Identité & Accès',
description: "APIs d'authentification et contrôle d'accès",
parentId: null,
orderIndex: 2,
}),
);
/* Catégories feuilles */
const produits = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Produits',
description: 'Catalogue de produits',
parentId: ecommerce.id,
orderIndex: 0,
}),
);
const commandes = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Commandes',
description: 'Gestion des commandes',
parentId: ecommerce.id,
orderIndex: 1,
}),
);
const notifications = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Notifications',
description: 'Événements et notifications utilisateur',
parentId: ecommerce.id,
orderIndex: 2,
}),
);
const evenements = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Événements',
description: "Événements d'infrastructure temps réel",
parentId: infrastructure.id,
orderIndex: 0,
}),
);
const auth = await this.categoryRepo.save(
this.categoryRepo.create({
name: 'Authentification',
description: 'Authentification et autorisation',
parentId: identite.id,
orderIndex: 0,
}),
);
/* APIs de démo */
const entries = await this.apiRepo.save([
this.apiRepo.create({
title: 'Swagger Petstore',
version: '3.0.4',
description:
'A sample API that uses a petstore as an example to demonstrate features in the OpenAPI specification.',
type: 'OPENAPI',
yamlContent: PETSTORE_YAML,
status: 'PENDING',
categoryId: produits.id,
}),
this.apiRepo.create({
title: 'Streetlights Kafka API',
version: '1.0.0',
description:
'The Smartylighting Streetlights API allows you to remotely manage the city lights using Kafka messaging.',
type: 'ASYNCAPI',
yamlContent: STREETLIGHTS_YAML,
status: 'PENDING',
categoryId: evenements.id,
}),
this.apiRepo.create({
title: 'Account Service',
version: '1.0.0',
description: "Service de gestion des comptes utilisateurs avec publication d'événements.",
type: 'ASYNCAPI',
yamlContent: ACCOUNT_SERVICE_YAML,
status: 'PENDING',
categoryId: notifications.id,
}),
this.apiRepo.create({
title: 'Orders API',
version: '1.0.0',
description: 'API de gestion des commandes clients.',
type: 'OPENAPI',
yamlContent: ORDERS_YAML,
status: 'PENDING',
categoryId: commandes.id,
}),
this.apiRepo.create({
title: 'Auth API',
version: '1.0.0',
description: "Service d'authentification et d'autorisation.",
type: 'OPENAPI',
yamlContent: AUTH_YAML,
status: 'PENDING',
categoryId: auth.id,
}),
]);
console.log('[Seeder] 8 catégories et 5 APIs de démo créées — génération docs en cours...');
/* Déclenche la génération de docs en fire-and-forget pour chaque API */
for (const entry of entries) {
this.docsService.generate(entry.id).catch(console.error);
}
}
}