Files
datacat/back/src/modules/seeder/seeder.service.ts
z3n 498c6577e4 feat(search): recherche intelligente — parsing backend + pg_trgm fuzzy
- parseSearchTokens() côté backend : extrait type (async/openapi),
  version (v1, 1.2.3) et latestOnly depuis la chaîne brute
- Recherche fuzzy via word_similarity (pg_trgm, seuil 0.5) + ILIKE
- latestOnly automatique quand champ non vide sans version explicite
- Suppression de parseSearch() dans BrowseComponent et CatalogComponent
- Suppression de version/latestOnly de ApiListQuery (géré par le backend)
- Activation de pg_trgm via CREATE EXTENSION dans SeederService
- Tests unitaires Vitest pour parseSearchTokens (24 cas)

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>
2026-06-17 08:57:45 +00:00

841 lines
24 KiB
TypeScript

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<ApiEntryEntity>,
@InjectRepository(CategoryEntity)
private readonly categoryRepo: Repository<CategoryEntity>,
private readonly docsService: DocsGenerationService,
private readonly dataSource: DataSource,
) {}
async onApplicationBootstrap(): Promise<void> {
// 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) 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(
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,
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,
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,
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);
}
}
}