feat: refonte métadonnées API + UX dossiers et import

- Nouveau schéma api_entries : convention+name, version X.X.X, provider,
  contacts splitées (name/entity/email), suppression des anciens champs
- Formulaire upload étape 2 : convention+nom, version 3 champs, fournisseur,
  contacts métier et technique obligatoires ; type détecté auto → select désactivé
- Validation YAML : redocly.yaml (extends: spec), support noms avec espaces,
  extraction métadonnées (name, version, description) depuis info YAML
- Dossiers : modale création avec champ Emplacement pré-initialisé depuis
  le contexte de navigation, options └ avec indentation par profondeur
- Import depuis un dossier : pré-sélection de la catégorie via queryParam
- Recherche étendue : name, provider, description, contacts
- Résolution @datacat/shared via workspace:* + compilation CJS avant NestJS

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-06-11 13:38:14 +00:00
parent ef3884c68d
commit e85696079c
20 changed files with 911 additions and 519 deletions

View File

@@ -4,14 +4,11 @@ import { Repository } from 'typeorm';
import { ApiEntryEntity } from './api-entry.entity';
import { CreateApiEntryDto } from './dto/create-api-entry.dto';
import { UpdateApiEntryDto } from './dto/update-api-entry.dto';
import { ApiListResponse, ApiListQuery, ApiEntry, ApiEntryListItem, ApiType, ApiStatus } from '@datacat/shared';
import { ApiListResponse, ApiListQuery, ApiEntry, ApiEntryListItem, ApiType, ApiStatus, ApiConvention, API_CONVENTION_LABELS } from '@datacat/shared';
import { DocsGenerationService } from '../docs-generation/docs-generation.service';
/** Champs internes mis à jour par DocsGenerationService */
interface InternalUpdateFields {
title?: string;
version?: string;
description?: string;
htmlPath?: string | null;
status?: ApiStatus;
errorMessage?: string | null;
@@ -28,19 +25,23 @@ export class ApisService {
async create(dto: CreateApiEntryDto): Promise<ApiEntry> {
const entity = this.repo.create({
title: dto.title,
version: dto.version,
convention: dto.convention,
name: dto.name,
provider: dto.provider,
versionMajor: dto.versionMajor,
versionMinor: dto.versionMinor,
versionPatch: dto.versionPatch,
description: dto.description ?? null,
type: dto.type,
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,
contactFunctionalName: dto.contactFunctionalName,
contactFunctionalEntity: dto.contactFunctionalEntity,
contactFunctionalEmail: dto.contactFunctionalEmail,
contactTechnicalName: dto.contactTechnicalName,
contactTechnicalEntity: dto.contactTechnicalEntity,
contactTechnicalEmail: dto.contactTechnicalEmail,
});
const saved = await this.repo.save(entity);
return toApiEntry(saved);
@@ -58,7 +59,11 @@ export class ApisService {
}
if (query.search) {
qb.andWhere(
'(LOWER(api.title) LIKE :search OR LOWER(api.description) LIKE :search)',
`(LOWER(api.name) LIKE :search
OR LOWER(api.provider) LIKE :search
OR LOWER(COALESCE(api.description, '')) LIKE :search
OR LOWER(api.contact_functional_name) LIKE :search
OR LOWER(api.contact_technical_name) LIKE :search)`,
{ search: `%${query.search.toLowerCase()}%` },
);
}
@@ -109,24 +114,38 @@ export class ApisService {
}
}
function buildTitle(convention: ApiConvention, name: string): string {
return `${API_CONVENTION_LABELS[convention]} ${name}`;
}
function buildVersion(major: number, minor: number, patch: number): string {
return `${major}.${minor}.${patch}`;
}
function toApiEntry(entity: ApiEntryEntity): ApiEntry {
return {
id: entity.id,
title: entity.title,
version: entity.version,
title: buildTitle(entity.convention, entity.name),
name: entity.name,
convention: entity.convention,
version: buildVersion(entity.versionMajor, entity.versionMinor, entity.versionPatch),
versionMajor: entity.versionMajor,
versionMinor: entity.versionMinor,
versionPatch: entity.versionPatch,
description: entity.description,
type: entity.type,
provider: entity.provider,
yamlContent: entity.yamlContent,
htmlPath: entity.htmlPath,
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,
contactFunctionalName: entity.contactFunctionalName,
contactFunctionalEntity: entity.contactFunctionalEntity,
contactFunctionalEmail: entity.contactFunctionalEmail,
contactTechnicalName: entity.contactTechnicalName,
contactTechnicalEntity: entity.contactTechnicalEntity,
contactTechnicalEmail: entity.contactTechnicalEmail,
createdAt: entity.createdAt.toISOString(),
updatedAt: entity.updatedAt.toISOString(),
};
@@ -135,10 +154,13 @@ function toApiEntry(entity: ApiEntryEntity): ApiEntry {
function toApiEntryListItem(entity: ApiEntryEntity): ApiEntryListItem {
return {
id: entity.id,
title: entity.title,
version: entity.version,
title: buildTitle(entity.convention, entity.name),
name: entity.name,
convention: entity.convention,
version: buildVersion(entity.versionMajor, entity.versionMinor, entity.versionPatch),
description: entity.description,
type: entity.type,
provider: entity.provider,
status: entity.status,
categoryId: entity.categoryId,
createdAt: entity.createdAt.toISOString(),