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:
730
back/src/modules/seeder/seeder.service.ts
Normal file
730
back/src/modules/seeder/seeder.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user