import { Injectable, OnApplicationBootstrap } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, DataSource } from 'typeorm'; import * as fs from 'fs/promises'; 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, @InjectRepository(CategoryEntity) private readonly categoryRepo: Repository, private readonly docsService: DocsGenerationService, private readonly dataSource: DataSource, ) {} async onApplicationBootstrap(): Promise { // Activer l'extension pg_trgm pour la recherche fuzzy await this.dataSource.query('CREATE EXTENSION IF NOT EXISTS pg_trgm'); const forceReseed = process.env.FORCE_RESEED === 'true'; if (forceReseed) { await this.reset(); } const count = await this.apiRepo.count(); if (count > 0) { await this.fixIsCurrentFlags(); return; } await this.seed(); } /** * Corrige les flags is_current après une migration : * si toutes les versions d'un même groupe (name+convention) sont marquées courantes, * on ne garde que la plus haute version. */ private async fixIsCurrentFlags(): Promise { await this.dataSource.query(` UPDATE api_entries SET is_current = false WHERE id IN ( SELECT a.id FROM api_entries a WHERE a.is_current = true AND EXISTS ( SELECT 1 FROM api_entries b WHERE b.name = a.name AND b.convention = a.convention AND b.is_current = true AND b.id != a.id AND ( b.version_major > a.version_major OR (b.version_major = a.version_major AND b.version_minor > a.version_minor) OR (b.version_major = a.version_major AND b.version_minor = a.version_minor AND b.version_patch > a.version_patch) ) ) ) `); } private async reset(): Promise { /* Supprimer toutes les entrées puis les catégories */ await this.apiRepo.delete({}); await this.categoryRepo.delete({}); /* Nettoyer le répertoire docs-output */ const docsDir = process.env.DOCS_OUTPUT_DIR ?? '/app/docs-output'; await fs.rm(docsDir, { recursive: true, force: true }); await fs.mkdir(docsDir, { recursive: true }); console.log('[Seeder] Reset complet — données supprimées, docs nettoyées.'); } private async seed(): Promise { /* 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({ convention: 'CONSULTER', name: 'Référentiel Produits', versionMajor: 3, versionMinor: 0, versionPatch: 4, isCurrent: true, 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, provider: 'DINUM', contactFunctionalName: 'Alice Martin', contactFunctionalEntity: 'Équipe Produit', contactFunctionalEmail: 'alice.martin@example.gouv.fr', contactTechnicalName: 'Bob Dupont', contactTechnicalEntity: 'Équipe API', contactTechnicalEmail: 'bob.dupont@example.gouv.fr', }), this.apiRepo.create({ convention: 'ETRE_NOTIFIE', name: 'Éclairage Urbain Kafka', versionMajor: 1, versionMinor: 0, versionPatch: 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, provider: 'Bureau Infrastructure', contactFunctionalName: 'Claire Leroy', contactFunctionalEntity: 'Bureau Infrastructure', contactFunctionalEmail: 'claire.leroy@example.gouv.fr', contactTechnicalName: 'David Chen', contactTechnicalEntity: 'Ops', contactTechnicalEmail: 'ops@example.com', }), this.apiRepo.create({ convention: 'ETRE_NOTIFIE', name: 'Comptes Utilisateurs', versionMajor: 1, versionMinor: 0, versionPatch: 0, description: "Service de gestion des comptes utilisateurs avec publication d'événements.", type: 'ASYNCAPI', yamlContent: ACCOUNT_SERVICE_YAML, status: 'PENDING', categoryId: notifications.id, provider: 'Équipe Comptes', contactFunctionalName: 'Emma Roux', contactFunctionalEntity: 'Équipe Comptes', contactFunctionalEmail: 'emma.roux@example.gouv.fr', contactTechnicalName: 'François Blanc', contactTechnicalEntity: 'Dev Core', contactTechnicalEmail: 'dev-core@example.com', }), this.apiRepo.create({ convention: 'ENREGISTRER', name: 'Commandes Clients', versionMajor: 1, versionMinor: 0, versionPatch: 0, description: 'API de gestion des commandes clients.', type: 'OPENAPI', yamlContent: ORDERS_YAML, status: 'PENDING', categoryId: commandes.id, provider: 'Équipe Commerce', contactFunctionalName: 'Gaëlle Moreau', contactFunctionalEntity: 'Équipe Commerce', contactFunctionalEmail: 'gaelle.moreau@example.gouv.fr', contactTechnicalName: 'Hugo Simon', contactTechnicalEntity: 'API Commerce', contactTechnicalEmail: 'api-orders@example.com', }), this.apiRepo.create({ convention: 'CONSULTER', name: 'Référentiel Produits', versionMajor: 3, versionMinor: 0, versionPatch: 3, isCurrent: false, 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, provider: 'DINUM', contactFunctionalName: 'Alice Martin', contactFunctionalEntity: 'Équipe Produit', contactFunctionalEmail: 'alice.martin@example.gouv.fr', contactTechnicalName: 'Bob Dupont', contactTechnicalEntity: 'Équipe API', contactTechnicalEmail: 'bob.dupont@example.gouv.fr', }), this.apiRepo.create({ convention: 'CONSULTER', name: 'Référentiel Produits', versionMajor: 3, versionMinor: 0, versionPatch: 2, isCurrent: false, 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, provider: 'DINUM', contactFunctionalName: 'Alice Martin', contactFunctionalEntity: 'Équipe Produit', contactFunctionalEmail: 'alice.martin@example.gouv.fr', contactTechnicalName: 'Bob Dupont', contactTechnicalEntity: 'Équipe API', contactTechnicalEmail: 'bob.dupont@example.gouv.fr', }), this.apiRepo.create({ convention: 'CONSULTER', name: 'Authentification JWT', versionMajor: 1, versionMinor: 0, versionPatch: 0, description: "Service d'authentification et d'autorisation.", type: 'OPENAPI', yamlContent: AUTH_YAML, status: 'PENDING', categoryId: auth.id, provider: 'Équipe Sécurité', contactFunctionalName: 'Isabelle Petit', contactFunctionalEntity: 'Équipe Sécurité', contactFunctionalEmail: 'isabelle.petit@example.gouv.fr', contactTechnicalName: 'Julien Bernard', contactTechnicalEntity: 'Sécurité SI', contactTechnicalEmail: 'securite@example.com', }), ]); console.log('[Seeder] 8 catégories et 7 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); } } }