feat(apis): ajout champs de documentation enrichie sur les API entries

- Nouveaux champs : functionalDoc, technicalDoc, externalDocUrl, contactFunctional, contactTechnical, accessRights
- Entity, DTOs, service et seeder mis à jour côté backend
- Page upload et api-detail mis à jour côté frontend

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 14:13:56 +00:00
parent 3abe5d2b85
commit 1bbb0c1125
9 changed files with 320 additions and 1 deletions

View File

@@ -33,6 +33,24 @@ export class ApiEntryEntity {
@Column({ nullable: true, type: 'varchar', length: 36, name: 'category_id' })
categoryId!: string | null;
@Column({ nullable: true, type: 'text', name: 'functional_doc' })
functionalDoc!: string | null;
@Column({ nullable: true, type: 'text', name: 'technical_doc' })
technicalDoc!: string | null;
@Column({ nullable: true, type: 'varchar', length: 500, name: 'external_doc_url' })
externalDocUrl!: string | null;
@Column({ nullable: true, type: 'varchar', length: 255, name: 'contact_functional' })
contactFunctional!: string | null;
@Column({ nullable: true, type: 'varchar', length: 255, name: 'contact_technical' })
contactTechnical!: string | null;
@Column({ nullable: true, type: 'text', name: 'access_rights' })
accessRights!: string | null;
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;

View File

@@ -35,6 +35,12 @@ export class ApisService {
yamlContent: dto.yamlContent,
status: 'PENDING',
categoryId: dto.categoryId ?? null,
functionalDoc: dto.functionalDoc ?? null,
technicalDoc: dto.technicalDoc ?? null,
externalDocUrl: dto.externalDocUrl ?? null,
contactFunctional: dto.contactFunctional ?? null,
contactTechnical: dto.contactTechnical ?? null,
accessRights: dto.accessRights ?? null,
});
const saved = await this.repo.save(entity);
return toApiEntry(saved);
@@ -115,6 +121,12 @@ function toApiEntry(entity: ApiEntryEntity): ApiEntry {
status: entity.status,
errorMessage: entity.errorMessage,
categoryId: entity.categoryId,
functionalDoc: entity.functionalDoc,
technicalDoc: entity.technicalDoc,
externalDocUrl: entity.externalDocUrl,
contactFunctional: entity.contactFunctional,
contactTechnical: entity.contactTechnical,
accessRights: entity.accessRights,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
};

View File

@@ -24,4 +24,28 @@ export class CreateApiEntryDto {
@IsUUID()
@IsOptional()
categoryId?: string;
@IsString()
@IsOptional()
functionalDoc?: string;
@IsString()
@IsOptional()
technicalDoc?: string;
@IsString()
@IsOptional()
externalDocUrl?: string;
@IsString()
@IsOptional()
contactFunctional?: string;
@IsString()
@IsOptional()
contactTechnical?: string;
@IsString()
@IsOptional()
accessRights?: string;
}

View File

@@ -16,4 +16,28 @@ export class UpdateApiEntryDto {
@IsUUID()
@IsOptional()
categoryId?: string | null;
@IsString()
@IsOptional()
functionalDoc?: string | null;
@IsString()
@IsOptional()
technicalDoc?: string | null;
@IsString()
@IsOptional()
externalDocUrl?: string | null;
@IsString()
@IsOptional()
contactFunctional?: string | null;
@IsString()
@IsOptional()
contactTechnical?: string | null;
@IsString()
@IsOptional()
accessRights?: string | null;
}

View File

@@ -1,6 +1,7 @@
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } 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';
@@ -595,11 +596,26 @@ export class SeederService implements OnApplicationBootstrap {
) {}
async onApplicationBootstrap(): Promise<void> {
const forceReseed = process.env.FORCE_RESEED === 'true';
if (forceReseed) {
await this.reset();
}
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 reset(): Promise<void> {
/* 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<void> {
/* Catégories racines */
const ecommerce = await this.categoryRepo.save(
@@ -680,6 +696,12 @@ export class SeederService implements OnApplicationBootstrap {
yamlContent: PETSTORE_YAML,
status: 'PENDING',
categoryId: produits.id,
functionalDoc: 'Cas d\'usage : gestion du catalogue produits. Permet de lister, créer et consulter des animaux de compagnie. Usage typique : intégration e-commerce, démo API REST.',
technicalDoc: 'Authentification Bearer token requise. Pagination via paramètre `limit` (max 100). Réponses JSON selon schéma OpenAPI 3.0. Endpoint base : https://petstore.swagger.io/v3.',
externalDocUrl: null,
contactFunctional: 'Équipe Produit',
contactTechnical: 'Équipe API',
accessRights: 'API publique — aucune habilitation requise.',
}),
this.apiRepo.create({
title: 'Streetlights Kafka API',
@@ -690,6 +712,12 @@ export class SeederService implements OnApplicationBootstrap {
yamlContent: STREETLIGHTS_YAML,
status: 'PENDING',
categoryId: evenements.id,
functionalDoc: 'Gestion de l\'éclairage urbain IoT. Permet de recevoir les mesures de luminosité des lampadaires et d\'envoyer des commandes d\'allumage/extinction à distance.',
technicalDoc: 'Broker Kafka sécurisé avec SCRAM-SHA-512. Topics : smartylighting.streetlights.1.0.*. Schémas de messages définis en AsyncAPI 3.0. Adresse broker : test.mykafkacluster.org:18092.',
externalDocUrl: null,
contactFunctional: 'Bureau Infrastructure',
contactTechnical: 'ops@example.com',
accessRights: 'Accès sur demande — habilitation DSI requise.',
}),
this.apiRepo.create({
title: 'Account Service',
@@ -699,6 +727,12 @@ export class SeederService implements OnApplicationBootstrap {
yamlContent: ACCOUNT_SERVICE_YAML,
status: 'PENDING',
categoryId: notifications.id,
functionalDoc: 'Cycle de vie du compte utilisateur. Publie des événements lors de l\'inscription et du changement de mot de passe. Utilisé par les services de notification et d\'audit.',
technicalDoc: 'Consommer via broker AMQP ou Kafka. Canaux : user/signedup, user/passwordchanged. Payloads JSON avec horodatage ISO 8601. Spec AsyncAPI 3.0.',
externalDocUrl: null,
contactFunctional: 'Équipe Comptes',
contactTechnical: 'dev-core@example.com',
accessRights: 'Interne uniquement — accès réservé aux services du SI.',
}),
this.apiRepo.create({
title: 'Orders API',
@@ -708,6 +742,12 @@ export class SeederService implements OnApplicationBootstrap {
yamlContent: ORDERS_YAML,
status: 'PENDING',
categoryId: commandes.id,
functionalDoc: 'Gestion des commandes clients : création, consultation, mise à jour du statut. Supporte les webhooks pour les transitions de statut (confirmed, shipped, delivered, cancelled).',
technicalDoc: 'REST + JWT Bearer. Pagination via paramètres `page` et `limit`. Webhook de statut : POST vers URL configurée lors de la transition. Base URL : https://api.example.com/v1.',
externalDocUrl: null,
contactFunctional: 'Équipe Commerce',
contactTechnical: 'api-orders@example.com',
accessRights: 'Partenaires agréés uniquement — convention de partenariat requise.',
}),
this.apiRepo.create({
title: 'Auth API',
@@ -717,6 +757,12 @@ export class SeederService implements OnApplicationBootstrap {
yamlContent: AUTH_YAML,
status: 'PENDING',
categoryId: auth.id,
functionalDoc: 'SSO centralisé avec tokens JWT. Gère la connexion, le rafraîchissement de token et la déconnexion. Exposé à tous les services du SI comme fournisseur d\'identité.',
technicalDoc: 'Bearer JWT (RS256). Endpoint principal : POST /auth/login. Refresh via POST /auth/refresh. Durée access token : 15 min. Durée refresh token : 7 jours. Base URL : https://auth.example.com/v1.',
externalDocUrl: null,
contactFunctional: 'Équipe Sécurité',
contactTechnical: 'securite@example.com',
accessRights: 'Restreint — habilitation RSSI requise avant intégration.',
}),
]);

View File

@@ -48,6 +48,12 @@ export class UploadsController {
description?: string;
type?: ApiType;
categoryId?: string;
functionalDoc?: string;
technicalDoc?: string;
externalDocUrl?: string;
contactFunctional?: string;
contactTechnical?: string;
accessRights?: string;
},
) {
if (!files || files.length === 0) throw new BadRequestException('YAML file is required');
@@ -86,6 +92,12 @@ export class UploadsController {
type: apiType,
yamlContent,
categoryId: body.categoryId || undefined,
functionalDoc: body.functionalDoc || undefined,
technicalDoc: body.technicalDoc || undefined,
externalDocUrl: body.externalDocUrl || undefined,
contactFunctional: body.contactFunctional || undefined,
contactTechnical: body.contactTechnical || undefined,
accessRights: body.accessRights || undefined,
});
/* Fire-and-forget : génération asynchrone */