From e85696079c41d922a1b36efe72c190f535d14797 Mon Sep 17 00:00:00 2001 From: z3n Date: Thu, 11 Jun 2026 13:38:14 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20refonte=20m=C3=A9tadonn=C3=A9es=20API?= =?UTF-8?q?=20+=20UX=20dossiers=20et=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 Co-Authored-By: Happy --- back/package.json | 1 + back/redocly.yaml | 3 + back/src/modules/apis/api-entry.entity.ts | 46 +- back/src/modules/apis/apis.service.ts | 68 ++- .../modules/apis/dto/create-api-entry.dto.ts | 59 ++- .../modules/apis/dto/update-api-entry.dto.ts | 46 +- .../modules/categories/categories.service.ts | 14 +- back/src/modules/seeder/seeder.service.ts | 100 ++-- .../src/modules/uploads/uploads.controller.ts | 81 ++-- back/tsconfig.json | 9 +- front-public/src/app/core/api.service.ts | 34 +- .../pages/api-detail/api-detail.component.ts | 340 ++++++++------ .../src/app/pages/browse/browse.component.ts | 69 ++- .../src/app/pages/upload/upload.component.ts | 441 +++++++++++------- infra/docker-compose.yml | 2 +- pnpm-lock.yaml | 51 +- shared/package.json | 7 +- shared/src/schemas/api-entry.schema.ts | 18 +- shared/src/types/api-entry.types.ts | 29 +- shared/tsconfig.build.json | 12 + 20 files changed, 911 insertions(+), 519 deletions(-) create mode 100644 back/redocly.yaml create mode 100644 shared/tsconfig.build.json diff --git a/back/package.json b/back/package.json index cd92f5b..d7adc04 100644 --- a/back/package.json +++ b/back/package.json @@ -12,6 +12,7 @@ "test:watch": "vitest" }, "dependencies": { + "@datacat/shared": "workspace:*", "@nestjs/common": "^11.0.0", "@nestjs/core": "^11.0.0", "@nestjs/platform-express": "^11.0.0", diff --git a/back/redocly.yaml b/back/redocly.yaml new file mode 100644 index 0000000..3859572 --- /dev/null +++ b/back/redocly.yaml @@ -0,0 +1,3 @@ +# Validation structurelle uniquement — pas de règles de style +extends: + - spec diff --git a/back/src/modules/apis/api-entry.entity.ts b/back/src/modules/apis/api-entry.entity.ts index 4d57969..a702650 100644 --- a/back/src/modules/apis/api-entry.entity.ts +++ b/back/src/modules/apis/api-entry.entity.ts @@ -1,16 +1,16 @@ import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; -import { ApiType, ApiStatus } from '@datacat/shared'; +import { ApiType, ApiStatus, ApiConvention } from '@datacat/shared'; @Entity('api_entries') export class ApiEntryEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column() - title!: string; + @Column({ type: 'varchar', length: 30, default: 'CONSULTER' }) + convention!: ApiConvention; - @Column() - version!: string; + @Column({ type: 'varchar', length: 255, default: '' }) + name!: string; @Column({ nullable: true, type: 'text' }) description!: string | null; @@ -18,6 +18,18 @@ export class ApiEntryEntity { @Column({ type: 'varchar', length: 10 }) type!: ApiType; + @Column({ type: 'varchar', length: 255, default: '', name: 'provider' }) + provider!: string; + + @Column({ type: 'int', default: 0, name: 'version_major' }) + versionMajor!: number; + + @Column({ type: 'int', default: 0, name: 'version_minor' }) + versionMinor!: number; + + @Column({ type: 'int', default: 0, name: 'version_patch' }) + versionPatch!: number; + @Column({ type: 'text', name: 'yaml_content' }) yamlContent!: string; @@ -33,23 +45,23 @@ 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({ type: 'varchar', length: 255, default: '', name: 'contact_functional_name' }) + contactFunctionalName!: string; - @Column({ nullable: true, type: 'text', name: 'technical_doc' }) - technicalDoc!: string | null; + @Column({ type: 'varchar', length: 255, default: '', name: 'contact_functional_entity' }) + contactFunctionalEntity!: string; - @Column({ nullable: true, type: 'varchar', length: 500, name: 'external_doc_url' }) - externalDocUrl!: string | null; + @Column({ type: 'varchar', length: 255, default: '', name: 'contact_functional_email' }) + contactFunctionalEmail!: string; - @Column({ nullable: true, type: 'varchar', length: 255, name: 'contact_functional' }) - contactFunctional!: string | null; + @Column({ type: 'varchar', length: 255, default: '', name: 'contact_technical_name' }) + contactTechnicalName!: string; - @Column({ nullable: true, type: 'varchar', length: 255, name: 'contact_technical' }) - contactTechnical!: string | null; + @Column({ type: 'varchar', length: 255, default: '', name: 'contact_technical_entity' }) + contactTechnicalEntity!: string; - @Column({ nullable: true, type: 'text', name: 'access_rights' }) - accessRights!: string | null; + @Column({ type: 'varchar', length: 255, default: '', name: 'contact_technical_email' }) + contactTechnicalEmail!: string; @CreateDateColumn({ name: 'created_at' }) createdAt!: Date; diff --git a/back/src/modules/apis/apis.service.ts b/back/src/modules/apis/apis.service.ts index e7e5e9d..2a27340 100644 --- a/back/src/modules/apis/apis.service.ts +++ b/back/src/modules/apis/apis.service.ts @@ -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 { 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(), diff --git a/back/src/modules/apis/dto/create-api-entry.dto.ts b/back/src/modules/apis/dto/create-api-entry.dto.ts index 0f74e92..3a30d25 100644 --- a/back/src/modules/apis/dto/create-api-entry.dto.ts +++ b/back/src/modules/apis/dto/create-api-entry.dto.ts @@ -1,14 +1,33 @@ -import { IsString, IsNotEmpty, IsIn, IsOptional, IsUUID } from 'class-validator'; -import { ApiType } from '@datacat/shared'; +import { IsString, IsNotEmpty, IsIn, IsOptional, IsUUID, IsInt, Min, IsEmail } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiType, ApiConvention } from '@datacat/shared'; export class CreateApiEntryDto { - @IsString() - @IsNotEmpty() - title!: string; + @IsIn(['CONSULTER', 'ENREGISTRER', 'ETRE_NOTIFIE']) + convention!: ApiConvention; @IsString() @IsNotEmpty() - version!: string; + name!: string; + + @IsString() + @IsNotEmpty() + provider!: string; + + @IsInt() + @Min(0) + @Type(() => Number) + versionMajor!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + versionMinor!: number; + + @IsInt() + @Min(0) + @Type(() => Number) + versionPatch!: number; @IsString() @IsOptional() @@ -26,26 +45,24 @@ export class CreateApiEntryDto { categoryId?: string; @IsString() - @IsOptional() - functionalDoc?: string; + @IsNotEmpty() + contactFunctionalName!: string; @IsString() - @IsOptional() - technicalDoc?: string; + @IsNotEmpty() + contactFunctionalEntity!: string; + + @IsEmail() + contactFunctionalEmail!: string; @IsString() - @IsOptional() - externalDocUrl?: string; + @IsNotEmpty() + contactTechnicalName!: string; @IsString() - @IsOptional() - contactFunctional?: string; + @IsNotEmpty() + contactTechnicalEntity!: string; - @IsString() - @IsOptional() - contactTechnical?: string; - - @IsString() - @IsOptional() - accessRights?: string; + @IsEmail() + contactTechnicalEmail!: string; } diff --git a/back/src/modules/apis/dto/update-api-entry.dto.ts b/back/src/modules/apis/dto/update-api-entry.dto.ts index 3f44e1a..cb6f5e5 100644 --- a/back/src/modules/apis/dto/update-api-entry.dto.ts +++ b/back/src/modules/apis/dto/update-api-entry.dto.ts @@ -1,17 +1,41 @@ -import { IsString, IsOptional, IsUUID } from 'class-validator'; +import { IsString, IsOptional, IsUUID, IsIn, IsInt, Min } from 'class-validator'; +import { Type } from 'class-transformer'; +import { ApiConvention } from '@datacat/shared'; export class UpdateApiEntryDto { - @IsString() + @IsIn(['CONSULTER', 'ENREGISTRER', 'ETRE_NOTIFIE']) @IsOptional() - title?: string; + convention?: ApiConvention; @IsString() @IsOptional() - version?: string; + name?: string; @IsString() @IsOptional() - description?: string; + provider?: string; + + @IsInt() + @Min(0) + @Type(() => Number) + @IsOptional() + versionMajor?: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @IsOptional() + versionMinor?: number; + + @IsInt() + @Min(0) + @Type(() => Number) + @IsOptional() + versionPatch?: number; + + @IsString() + @IsOptional() + description?: string | null; @IsUUID() @IsOptional() @@ -19,25 +43,25 @@ export class UpdateApiEntryDto { @IsString() @IsOptional() - functionalDoc?: string | null; + contactFunctionalName?: string; @IsString() @IsOptional() - technicalDoc?: string | null; + contactFunctionalEntity?: string; @IsString() @IsOptional() - externalDocUrl?: string | null; + contactFunctionalEmail?: string; @IsString() @IsOptional() - contactFunctional?: string | null; + contactTechnicalName?: string; @IsString() @IsOptional() - contactTechnical?: string | null; + contactTechnicalEntity?: string; @IsString() @IsOptional() - accessRights?: string | null; + contactTechnicalEmail?: string; } diff --git a/back/src/modules/categories/categories.service.ts b/back/src/modules/categories/categories.service.ts index 04286d6..c3280ef 100644 --- a/back/src/modules/categories/categories.service.ts +++ b/back/src/modules/categories/categories.service.ts @@ -13,6 +13,7 @@ import { ApiEntryListItem, ApiType, ApiStatus, + ApiConvention, } from '@datacat/shared'; @Injectable() @@ -150,13 +151,22 @@ function toCategory(entity: CategoryEntity): Category { }; } +const CONVENTION_LABELS: Record = { + CONSULTER: 'Consulter', + ENREGISTRER: 'Enregistrer', + ETRE_NOTIFIE: 'Être notifié', +}; + function toApiEntryListItem(entity: ApiEntryEntity): ApiEntryListItem { return { id: entity.id, - title: entity.title, - version: entity.version, + title: `${CONVENTION_LABELS[entity.convention] ?? entity.convention} ${entity.name}`, + name: entity.name, + convention: entity.convention, + version: `${entity.versionMajor}.${entity.versionMinor}.${entity.versionPatch}`, description: entity.description, type: entity.type as ApiType, + provider: entity.provider, status: entity.status as ApiStatus, categoryId: entity.categoryId, createdAt: entity.createdAt.toISOString(), diff --git a/back/src/modules/seeder/seeder.service.ts b/back/src/modules/seeder/seeder.service.ts index fdd1db9..00b39ca 100644 --- a/back/src/modules/seeder/seeder.service.ts +++ b/back/src/modules/seeder/seeder.service.ts @@ -688,81 +688,101 @@ export class SeederService implements OnApplicationBootstrap { /* APIs de démo */ const entries = await this.apiRepo.save([ this.apiRepo.create({ - title: 'Swagger Petstore', - version: '3.0.4', + 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, - 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.', + 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({ - title: 'Streetlights Kafka API', - version: '1.0.0', + 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, - 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.', + 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({ - title: 'Account Service', - version: '1.0.0', + 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, - 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.', + 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({ - title: 'Orders API', - version: '1.0.0', + 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, - 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.', + 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({ - title: 'Auth API', - version: '1.0.0', + 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, - 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.', + 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', }), ]); diff --git a/back/src/modules/uploads/uploads.controller.ts b/back/src/modules/uploads/uploads.controller.ts index 476d284..1d80489 100644 --- a/back/src/modules/uploads/uploads.controller.ts +++ b/back/src/modules/uploads/uploads.controller.ts @@ -57,13 +57,15 @@ export class UploadsController { valid: boolean; type: ApiType | null; mainFile: string | null; - title: string | null; - version: string | null; + name: string | null; + versionMajor: number | null; + versionMinor: number | null; + versionPatch: number | null; description: string | null; errors: Array<{ file: string; message: string }>; }> { if (!files || files.length === 0) { - return { valid: false, type: null, mainFile: null, title: null, version: null, description: null, errors: [{ file: '', message: 'Aucun fichier fourni.' }] }; + return { valid: false, type: null, mainFile: null, name: null, versionMajor: null, versionMinor: null, versionPatch: null, description: null, errors: [{ file: '', message: 'Aucun fichier fourni.' }] }; } /* Lire tous les contenus en mémoire avant toute suppression */ @@ -94,7 +96,7 @@ export class UploadsController { } if (errors.length > 0) { - return { valid: false, type: null, mainFile: null, title: null, version: null, description: null, errors }; + return { valid: false, type: null, mainFile: null, name: null, versionMajor: null, versionMinor: null, versionPatch: null, description: null, errors }; } /* Détection du fichier principal */ @@ -112,7 +114,7 @@ export class UploadsController { if (!mainFileName) { return { - valid: false, type: null, mainFile: null, title: null, version: null, description: null, + valid: false, type: null, mainFile: null, name: null, versionMajor: null, versionMinor: null, versionPatch: null, description: null, errors: [{ file: '', message: 'Aucun fichier principal trouvé (clé openapi: ou asyncapi: manquante).' }], }; } @@ -133,14 +135,14 @@ export class UploadsController { if (detectedType === 'ASYNCAPI') { await execFileAsync('asyncapi', ['validate', mainPath], { env: execEnv, timeout: 30000 }); } else { - await execFileAsync('redocly', ['lint', mainPath], { shell: true, env: execEnv, timeout: 30000 }); + await execFileAsync('redocly', ['lint', mainPath], { env: execEnv, timeout: 30000 }); } } catch (cliError) { const err = cliError as { stdout?: string; stderr?: string }; - const output = (err.stdout ?? err.stderr ?? String(cliError)).trim(); + const output = (err.stdout || err.stderr || String(cliError)).trim(); return { valid: false, type: detectedType, mainFile: mainFileName, - title: null, version: null, description: null, + name: null, versionMajor: null, versionMinor: null, versionPatch: null, description: null, errors: [{ file: mainFileName, message: output }], }; } @@ -152,11 +154,22 @@ export class UploadsController { const mainContent = fileContents.find((f) => f.name === mainFileName)!.content; const parsed = yaml.load(mainContent) as Record; const info = parsed['info'] as Record | undefined; - const title = typeof info?.['title'] === 'string' ? info['title'] : null; - const version = typeof info?.['version'] === 'string' ? String(info['version']) : null; + const name = typeof info?.['title'] === 'string' ? info['title'] : null; const description = typeof info?.['description'] === 'string' ? info['description'] : null; - return { valid: true, type: detectedType, mainFile: mainFileName, title, version, description, errors: [] }; + /* Décomposer la version en major.minor.patch */ + let versionMajor: number | null = null; + let versionMinor: number | null = null; + let versionPatch: number | null = null; + const versionStr = typeof info?.['version'] === 'string' ? info['version'] : null; + if (versionStr) { + const parts = versionStr.split('.').map((p) => parseInt(p, 10)); + if (parts.length >= 1 && !isNaN(parts[0])) versionMajor = parts[0]; + if (parts.length >= 2 && !isNaN(parts[1])) versionMinor = parts[1]; + if (parts.length >= 3 && !isNaN(parts[2])) versionPatch = parts[2]; + } + + return { valid: true, type: detectedType, mainFile: mainFileName, name, versionMajor, versionMinor, versionPatch, description, errors: [] }; } @Post() @@ -175,22 +188,26 @@ export class UploadsController { @UploadedFiles() files: Express.Multer.File[], @Body() body: { - title: string; - version: string; + convention: string; + name: string; + provider: string; + versionMajor: string; + versionMinor: string; + versionPatch: string; description?: string; type?: ApiType; categoryId?: string; - functionalDoc?: string; - technicalDoc?: string; - externalDocUrl?: string; - contactFunctional?: string; - contactTechnical?: string; - accessRights?: string; + contactFunctionalName: string; + contactFunctionalEntity: string; + contactFunctionalEmail: string; + contactTechnicalName: string; + contactTechnicalEntity: string; + contactTechnicalEmail: string; }, ) { if (!files || files.length === 0) throw new BadRequestException('YAML file is required'); - if (!body.title) throw new BadRequestException('title is required'); - if (!body.version) throw new BadRequestException('version is required'); + if (!body.name) throw new BadRequestException('name is required'); + if (!body.convention) throw new BadRequestException('convention is required'); /* Crée le répertoire si absent */ const uploadDir = path.join(os.tmpdir(), 'datacat-uploads'); @@ -225,18 +242,22 @@ export class UploadsController { } const entry = await this.apisService.create({ - title: body.title, - version: body.version, + convention: (body.convention as 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE') ?? 'CONSULTER', + name: body.name, + provider: body.provider, + versionMajor: parseInt(body.versionMajor, 10) || 0, + versionMinor: parseInt(body.versionMinor, 10) || 0, + versionPatch: parseInt(body.versionPatch, 10) || 0, description: body.description, 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, + contactFunctionalName: body.contactFunctionalName, + contactFunctionalEntity: body.contactFunctionalEntity, + contactFunctionalEmail: body.contactFunctionalEmail, + contactTechnicalName: body.contactTechnicalName, + contactTechnicalEntity: body.contactTechnicalEntity, + contactTechnicalEmail: body.contactTechnicalEmail, }); /* Fire-and-forget : génération asynchrone */ @@ -300,7 +321,7 @@ export class UploadsController { if (type === 'OPENAPI') { /* redocly bundle : pas d'appel réseau, fonctionne en local */ - await execFileAsync('redocly', ['bundle', mainPath, '--output', bundledPath], { shell: true, env: execEnv }); + await execFileAsync('redocly', ['bundle', mainPath, '--output', bundledPath], { env: execEnv }); return await fsPromises.readFile(bundledPath, 'utf-8'); } else { /* @asyncapi/bundler API programmatique : aucun appel réseau */ diff --git a/back/tsconfig.json b/back/tsconfig.json index 76d5dc4..41a70c3 100644 --- a/back/tsconfig.json +++ b/back/tsconfig.json @@ -10,10 +10,9 @@ "target": "ES2021", "sourceMap": true, "outDir": "./dist", + "rootDir": "../", "baseUrl": "./", - "incremental": true, - "paths": { - "@datacat/shared": ["../shared/src/index.ts"] - } - } + "incremental": true + }, + "include": ["src/**/*.ts"] } diff --git a/front-public/src/app/core/api.service.ts b/front-public/src/app/core/api.service.ts index bd44268..9a4a201 100644 --- a/front-public/src/app/core/api.service.ts +++ b/front-public/src/app/core/api.service.ts @@ -23,16 +23,20 @@ export class ApiService { } update(id: string, data: { - title?: string; - version?: string; - description?: string; + convention?: string; + name?: string; + provider?: string; + versionMajor?: number; + versionMinor?: number; + versionPatch?: number; + description?: string | null; categoryId?: string | null; - functionalDoc?: string | null; - technicalDoc?: string | null; - externalDocUrl?: string | null; - contactFunctional?: string | null; - contactTechnical?: string | null; - accessRights?: string | null; + contactFunctionalName?: string; + contactFunctionalEntity?: string; + contactFunctionalEmail?: string; + contactTechnicalName?: string; + contactTechnicalEntity?: string; + contactTechnicalEmail?: string; }): Observable { return this.http.patch(`${this.baseUrl}/${id}`, data); } @@ -53,8 +57,10 @@ export class ApiService { valid: boolean; type: 'ASYNCAPI' | 'OPENAPI' | null; mainFile: string | null; - title: string | null; - version: string | null; + name: string | null; + versionMajor: number | null; + versionMinor: number | null; + versionPatch: number | null; description: string | null; errors: Array<{ file: string; message: string }>; }> { @@ -62,8 +68,10 @@ export class ApiService { valid: boolean; type: 'ASYNCAPI' | 'OPENAPI' | null; mainFile: string | null; - title: string | null; - version: string | null; + name: string | null; + versionMajor: number | null; + versionMinor: number | null; + versionPatch: number | null; description: string | null; errors: Array<{ file: string; message: string }>; }>('/api/uploads/validate', formData); diff --git a/front-public/src/app/pages/api-detail/api-detail.component.ts b/front-public/src/app/pages/api-detail/api-detail.component.ts index d5d9a72..7c6a04c 100644 --- a/front-public/src/app/pages/api-detail/api-detail.component.ts +++ b/front-public/src/app/pages/api-detail/api-detail.component.ts @@ -10,7 +10,7 @@ import { CommonModule } from '@angular/common'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ApiService } from '../../core/api.service'; import { CategoryService } from '../../core/category.service'; -import { ApiEntry, Category } from '@datacat/shared'; +import { ApiEntry, Category, ApiConvention } from '@datacat/shared'; @Component({ selector: 'app-api-detail', @@ -81,10 +81,6 @@ import { ApiEntry, Category } from '@datacat/shared';

Informations

-
-
Titre
-
{{ entry.title }}
-
Version
{{ entry.version }}
@@ -93,6 +89,10 @@ import { ApiEntry, Category } from '@datacat/shared';
Type
{{ entry.type }}
+
+
Fournisseur
+
{{ entry.provider }}
+
@if (entry.description) {
Description
@@ -112,80 +112,28 @@ import { ApiEntry, Category } from '@datacat/shared';
- - @if (entry.functionalDoc) { -
-
-
-

Documentation fonctionnelle

-

{{ entry.functionalDoc }}

-
-
-
- } - - - @if (entry.technicalDoc) { -
-
-
-

Documentation technique

-

{{ entry.technicalDoc }}

-
-
-
- } - - @if (entry.contactFunctional || entry.contactTechnical) { -
-
-
-

Contacts

-
- @if (entry.contactFunctional) { -
-
Contact fonctionnel
-
{{ entry.contactFunctional }}
-
- } - @if (entry.contactTechnical) { -
-
Contact technique
-
{{ entry.contactTechnical }}
-
- } -
+
+
+
+

Contacts

+
+
+

Contact métier

+

{{ entry.contactFunctionalName }}

+

{{ entry.contactFunctionalEntity }}

+

{{ entry.contactFunctionalEmail }}

+
+
+

Contact technique

+

{{ entry.contactTechnicalName }}

+

{{ entry.contactTechnicalEntity }}

+

{{ entry.contactTechnicalEmail }}

+
- } - - - @if (entry.accessRights) { -
-
-
-

Droits d'accès

-

{{ entry.accessRights }}

-
-
-
- } - - - @if (entry.externalDocUrl) { - - } +
@@ -229,7 +177,7 @@ import { ApiEntry, Category } from '@datacat/shared'; @if (showEditModal()) {
-
+
@@ -242,57 +190,144 @@ import { ApiEntry, Category } from '@datacat/shared'; @if (editError()) {

{{ editError() }}

} -
- - - @if (editTitleError()) {

Le titre est obligatoire.

} -
-
- - - @if (editVersionError()) {

La version est obligatoire.

} + + +

Titre *

+
+
+
+ + +
+
+
+
+ + + @if (editNameError()) {

Le nom est obligatoire.

} +
+
+

Titre : {{ editPreviewTitle() }}

+ +
- @for (cat of categories(); track cat.id) { }
+ + +
+ + + @if (editProviderError()) {

Le fournisseur est obligatoire.

} +
+ +
- +
-
- - + + +

Version *

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
-
- - + + +

Contact métier *

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
-
- - -
-
- - -
-
- - -
-
- - + + +

Contact technique *

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
@@ -246,6 +251,18 @@ import { forkJoin } from 'rxjs';

{{ formError() }}

} + @if (formMode() === 'create') { +
+ + +
+ }
+

Titre complet : {{ previewTitle() }}

+
+ Catégorie +
+ +
+ + + @if (providerInvalid()) { +

Le fournisseur est obligatoire.

+ } +
+ + +
+ + + @if (detectedType()) { +

Détecté automatiquement depuis le fichier YAML

+ } +
+ +
+ Description +
-
- - + +

Version *

+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
-
- - + +

Contact métier *

+
+
+
+ + + @if (fieldInvalid('contactFunctionalName')) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (fieldInvalid('contactFunctionalEntity')) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (fieldInvalid('contactFunctionalEmail')) { +

Email valide requis.

+ } +
+
-
- - -
- -
- - -
- -
- - -
- -
- - + +

Contact technique *

+
+
+
+ + + @if (fieldInvalid('contactTechnicalName')) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (fieldInvalid('contactTechnicalEntity')) { +

Obligatoire.

+ } +
+
+
+
+ + + @if (fieldInvalid('contactTechnicalEmail')) { +

Email valide requis.

+ } +
+
@@ -303,13 +383,17 @@ import { Category } from '@datacat/shared';
Type
{{ metaForm.value.type }}
-
+
Titre
-
{{ metaForm.value.title }}
+
{{ previewTitle() }}
Version
-
{{ metaForm.value.version }}
+
{{ metaForm.value.versionMajor }}.{{ metaForm.value.versionMinor }}.{{ metaForm.value.versionPatch }}
+
+
+
Fournisseur
+
{{ metaForm.value.provider }}
@if (metaForm.value.categoryId) {
@@ -356,6 +440,7 @@ export class UploadComponent implements OnInit { private apiService = inject(ApiService); private categoryService = inject(CategoryService); private router = inject(Router); + private route = inject(ActivatedRoute); step = signal(1); selectedFiles = signal([]); @@ -376,30 +461,57 @@ export class UploadComponent implements OnInit { ]; metaForm = this.fb.group({ - title: ['', Validators.required], - version: ['', Validators.required], + convention: ['CONSULTER', Validators.required], + name: ['', Validators.required], + provider: ['', Validators.required], + versionMajor: [0, [Validators.required, Validators.min(0)]], + versionMinor: [0, [Validators.required, Validators.min(0)]], + versionPatch: [0, [Validators.required, Validators.min(0)]], type: ['ASYNCAPI', Validators.required], categoryId: [''], description: [''], - functionalDoc: [''], - technicalDoc: [''], - externalDocUrl: [''], - contactFunctional: [''], - contactTechnical: [''], - accessRights: [''], + contactFunctionalName: ['', Validators.required], + contactFunctionalEntity: ['', Validators.required], + contactFunctionalEmail: ['', [Validators.required, Validators.email]], + contactTechnicalName: ['', Validators.required], + contactTechnicalEntity: ['', Validators.required], + contactTechnicalEmail: ['', [Validators.required, Validators.email]], }); - titleInvalid = computed( - () => !!(this.metaForm.get('title')?.invalid && this.metaForm.get('title')?.touched), - ); - versionInvalid = computed( - () => !!(this.metaForm.get('version')?.invalid && this.metaForm.get('version')?.touched), - ); + /* Convertit valueChanges en signal pour que les computed() trackent les changements du formulaire */ + private readonly formValues = toSignal(this.metaForm.valueChanges, { initialValue: this.metaForm.value }); + + previewTitle = computed(() => { + const values = this.formValues(); + const convention = (values.convention ?? 'CONSULTER') as ApiConvention; + const name = values.name ?? ''; + return `${API_CONVENTION_LABELS[convention]} ${name}`.trim(); + }); selectedCategoryName = computed(() => { - const id = this.metaForm.value.categoryId; + const id = this.formValues().categoryId; if (!id) return ''; return this.categories().find((c) => c.id === id)?.name ?? ''; }); + + /* Méthodes de validation (pas de computed — la re-exécution est déclenchée par les événements DOM blur/input) */ + conventionInvalid(): boolean { + return !!(this.metaForm.get('convention')?.invalid && this.metaForm.get('convention')?.touched); + } + nameInvalid(): boolean { + return !!(this.metaForm.get('name')?.invalid && this.metaForm.get('name')?.touched); + } + providerInvalid(): boolean { + return !!(this.metaForm.get('provider')?.invalid && this.metaForm.get('provider')?.touched); + } + versionMajorInvalid(): boolean { + return !!(this.metaForm.get('versionMajor')?.invalid && this.metaForm.get('versionMajor')?.touched); + } + versionMinorInvalid(): boolean { + return !!(this.metaForm.get('versionMinor')?.invalid && this.metaForm.get('versionMinor')?.touched); + } + versionPatchInvalid(): boolean { + return !!(this.metaForm.get('versionPatch')?.invalid && this.metaForm.get('versionPatch')?.touched); + } canProceedFromStep1 = computed(() => { /* Sans fichier, on peut toujours passer à l'étape 2 */ if (this.selectedFiles().length === 0) return true; @@ -408,9 +520,20 @@ export class UploadComponent implements OnInit { return this.validationDone() && this.validationErrors().length === 0; }); + fieldInvalid(name: string): boolean { + const ctrl = this.metaForm.get(name); + return !!(ctrl?.invalid && ctrl?.touched); + } + ngOnInit() { + const preselectedCategoryId = this.route.snapshot.queryParamMap.get('categoryId'); this.categoryService.list().subscribe({ - next: (res) => this.categories.set(res.items), + next: (res) => { + this.categories.set(res.items); + if (preselectedCategoryId) { + this.metaForm.patchValue({ categoryId: preselectedCategoryId }); + } + }, }); } @@ -426,6 +549,7 @@ export class UploadComponent implements OnInit { this.detectedMainFilename.set(null); this.validationErrors.set([]); this.validationDone.set(false); + this.metaForm.get('type')?.enable(); const input = event.target as HTMLInputElement; const fileList = input.files; @@ -471,18 +595,22 @@ export class UploadComponent implements OnInit { this.validationDone.set(true); if (!result.valid) { this.validationErrors.set(result.errors); + this.metaForm.get('type')?.enable(); } else { this.validationErrors.set([]); if (result.type) { this.detectedType.set(result.type); this.metaForm.patchValue({ type: result.type }); + this.metaForm.get('type')?.disable(); } if (result.mainFile) { this.detectedMainFilename.set(result.mainFile); } /* Pré-remplissage du formulaire depuis les métadonnées du fichier */ - if (result.title) this.metaForm.patchValue({ title: result.title }); - if (result.version) this.metaForm.patchValue({ version: String(result.version) }); + if (result.name) this.metaForm.patchValue({ name: result.name }); + if (result.versionMajor !== null) this.metaForm.patchValue({ versionMajor: result.versionMajor }); + if (result.versionMinor !== null) this.metaForm.patchValue({ versionMinor: result.versionMinor }); + if (result.versionPatch !== null) this.metaForm.patchValue({ versionPatch: result.versionPatch }); if (result.description) this.metaForm.patchValue({ description: result.description }); } }, @@ -509,37 +637,30 @@ export class UploadComponent implements OnInit { this.uploading.set(true); this.uploadError.set(null); + const raw = this.metaForm.getRawValue(); const formData = new FormData(); for (const f of files) { formData.append('files', f); } - formData.append('title', this.metaForm.value.title ?? ''); - formData.append('version', this.metaForm.value.version ?? ''); - formData.append('type', this.metaForm.value.type ?? 'ASYNCAPI'); - if (this.metaForm.value.categoryId) { - formData.append('categoryId', this.metaForm.value.categoryId); + formData.append('convention', raw.convention ?? 'CONSULTER'); + formData.append('name', raw.name ?? ''); + formData.append('provider', raw.provider ?? ''); + formData.append('versionMajor', String(raw.versionMajor ?? 0)); + formData.append('versionMinor', String(raw.versionMinor ?? 0)); + formData.append('versionPatch', String(raw.versionPatch ?? 0)); + formData.append('type', raw.type ?? 'ASYNCAPI'); + if (raw.categoryId) { + formData.append('categoryId', raw.categoryId); } - if (this.metaForm.value.description) { - formData.append('description', this.metaForm.value.description); - } - if (this.metaForm.value.functionalDoc) { - formData.append('functionalDoc', this.metaForm.value.functionalDoc); - } - if (this.metaForm.value.technicalDoc) { - formData.append('technicalDoc', this.metaForm.value.technicalDoc); - } - if (this.metaForm.value.externalDocUrl) { - formData.append('externalDocUrl', this.metaForm.value.externalDocUrl); - } - if (this.metaForm.value.contactFunctional) { - formData.append('contactFunctional', this.metaForm.value.contactFunctional); - } - if (this.metaForm.value.contactTechnical) { - formData.append('contactTechnical', this.metaForm.value.contactTechnical); - } - if (this.metaForm.value.accessRights) { - formData.append('accessRights', this.metaForm.value.accessRights); + if (raw.description) { + formData.append('description', raw.description); } + formData.append('contactFunctionalName', raw.contactFunctionalName ?? ''); + formData.append('contactFunctionalEntity', raw.contactFunctionalEntity ?? ''); + formData.append('contactFunctionalEmail', raw.contactFunctionalEmail ?? ''); + formData.append('contactTechnicalName', raw.contactTechnicalName ?? ''); + formData.append('contactTechnicalEntity', raw.contactTechnicalEntity ?? ''); + formData.append('contactTechnicalEmail', raw.contactTechnicalEmail ?? ''); this.apiService.upload(formData).subscribe({ next: (entry) => { diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 1a99b70..7845e7c 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -21,7 +21,7 @@ services: NODE_ENV: development DOCS_OUTPUT_DIR: /app/docs-output working_dir: /workspace/back - command: sh -c "cd /workspace && pnpm install && cd /workspace/back && pnpm run start:dev" + command: sh -c "cd /workspace && pnpm install && /workspace/back/node_modules/.bin/tsc --project /workspace/shared/tsconfig.build.json && cd /workspace/back && pnpm run start:dev" networks: - network depends_on: diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 626db40..7ce2efc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,6 +22,9 @@ importers: '@asyncapi/html-template': specifier: 3.5.6 version: 3.5.6(@noble/hashes@1.8.0)(encoding@0.1.13)(typescript@5.9.3) + '@datacat/shared': + specifier: workspace:* + version: link:../shared '@nestjs/common': specifier: ^11.0.0 version: 11.1.22(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -10189,7 +10192,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.1902.26(chokidar@4.0.3) - '@angular-devkit/build-webpack': 0.1902.26(chokidar@4.0.3)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)))(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + '@angular-devkit/build-webpack': 0.1902.26(chokidar@4.0.3)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)))(webpack@5.105.0(postcss@8.5.12)) '@angular-devkit/core': 19.2.26(chokidar@4.0.3) '@angular/build': 19.2.26(@angular/compiler-cli@19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3))(@angular/compiler@19.2.22)(@types/node@22.19.19)(chokidar@4.0.3)(jiti@1.21.7)(less@4.2.2)(postcss@8.5.12)(tailwindcss@3.3.3(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.8.3)))(terser@5.39.0)(typescript@5.8.3)(yaml@2.9.0) '@angular/compiler-cli': 19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3) @@ -10203,14 +10206,14 @@ snapshots: '@babel/preset-env': 7.26.9(@babel/core@7.26.10) '@babel/runtime': 7.26.10 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 19.2.26(@angular/compiler-cli@19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + '@ngtools/webpack': 19.2.26(@angular/compiler-cli@19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.105.0(postcss@8.5.12)) '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.3.3(@types/node@22.19.19)(jiti@1.21.7)(less@4.2.2)(sass@1.99.0)(terser@5.39.0)(yaml@2.9.0)) ansi-colors: 4.1.3 autoprefixer: 10.4.20(postcss@8.5.12) - babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + babel-loader: 9.2.1(@babel/core@7.26.10)(webpack@5.105.0(postcss@8.5.12)) browserslist: 4.28.2 - copy-webpack-plugin: 12.0.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) - css-loader: 7.1.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + copy-webpack-plugin: 12.0.2(webpack@5.105.0(postcss@8.5.12)) + css-loader: 7.1.2(webpack@5.105.0(postcss@8.5.12)) esbuild-wasm: 0.28.0 fast-glob: 3.3.3 http-proxy-middleware: 3.0.5 @@ -10218,22 +10221,22 @@ snapshots: jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.2.2 - less-loader: 12.2.0(less@4.2.2)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) - license-webpack-plugin: 4.0.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + less-loader: 12.2.0(less@4.2.2)(webpack@5.105.0(postcss@8.5.12)) + license-webpack-plugin: 4.0.2(webpack@5.105.0(postcss@8.5.12)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.9.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + mini-css-extract-plugin: 2.9.2(webpack@5.105.0(postcss@8.5.12)) open: 10.1.0 ora: 5.4.1 picomatch: 4.0.4 piscina: 4.8.0 postcss: 8.5.12 - postcss-loader: 8.1.1(postcss@8.5.12)(typescript@5.8.3)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + postcss-loader: 8.1.1(postcss@8.5.12)(typescript@5.8.3)(webpack@5.105.0(postcss@8.5.12)) resolve-url-loader: 5.0.0 rxjs: 7.8.1 sass: 1.85.0 - sass-loader: 16.0.5(sass@1.85.0)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + sass-loader: 16.0.5(sass@1.85.0)(webpack@5.105.0(postcss@8.5.12)) semver: 7.7.1 - source-map-loader: 5.0.0(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + source-map-loader: 5.0.0(webpack@5.105.0(postcss@8.5.12)) source-map-support: 0.5.21 terser: 5.39.0 tree-kill: 1.2.2 @@ -10243,7 +10246,7 @@ snapshots: webpack-dev-middleware: 7.4.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)) webpack-dev-server: 5.2.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)) webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)) + webpack-subresource-integrity: 5.1.0(webpack@5.105.0(postcss@8.5.12)) optionalDependencies: esbuild: 0.28.0 tailwindcss: 3.3.3(ts-node@10.9.2(@types/node@22.19.19)(typescript@5.8.3)) @@ -10277,7 +10280,7 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-webpack@0.1902.26(chokidar@4.0.3)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)))(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12))': + '@angular-devkit/build-webpack@0.1902.26(chokidar@4.0.3)(webpack-dev-server@5.2.2(tslib@2.8.1)(webpack@5.105.0(postcss@8.5.12)))(webpack@5.105.0(postcss@8.5.12))': dependencies: '@angular-devkit/architect': 0.1902.26(chokidar@4.0.3) rxjs: 7.8.1 @@ -13785,7 +13788,7 @@ snapshots: '@next/swc-win32-x64-msvc@14.2.33': optional: true - '@ngtools/webpack@19.2.26(@angular/compiler-cli@19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12))': + '@ngtools/webpack@19.2.26(@angular/compiler-cli@19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3))(typescript@5.8.3)(webpack@5.105.0(postcss@8.5.12))': dependencies: '@angular/compiler-cli': 19.2.22(@angular/compiler@19.2.22)(typescript@5.8.3) typescript: 5.8.3 @@ -16051,7 +16054,7 @@ snapshots: b4a@1.8.1: {} - babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + babel-loader@9.2.1(@babel/core@7.26.10)(webpack@5.105.0(postcss@8.5.12)): dependencies: '@babel/core': 7.26.10 find-cache-dir: 4.0.0 @@ -16729,7 +16732,7 @@ snapshots: dependencies: is-what: 3.14.1 - copy-webpack-plugin@12.0.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + copy-webpack-plugin@12.0.2(webpack@5.105.0(postcss@8.5.12)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -16804,7 +16807,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@7.1.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + css-loader@7.1.2(webpack@5.105.0(postcss@8.5.12)): dependencies: icss-utils: 5.1.0(postcss@8.5.12) postcss: 8.5.12 @@ -18750,7 +18753,7 @@ snapshots: dependencies: readable-stream: 2.3.8 - less-loader@12.2.0(less@4.2.2)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + less-loader@12.2.0(less@4.2.2)(webpack@5.105.0(postcss@8.5.12)): dependencies: less: 4.2.2 optionalDependencies: @@ -18776,7 +18779,7 @@ snapshots: libphonenumber-js@1.13.2: {} - license-webpack-plugin@4.0.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + license-webpack-plugin@4.0.2(webpack@5.105.0(postcss@8.5.12)): dependencies: webpack-sources: 3.4.1 optionalDependencies: @@ -19068,7 +19071,7 @@ snapshots: mimic-response@4.0.0: {} - mini-css-extract-plugin@2.9.2(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + mini-css-extract-plugin@2.9.2(webpack@5.105.0(postcss@8.5.12)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 @@ -20167,7 +20170,7 @@ snapshots: postcss: 8.5.15 ts-node: 10.9.2(@types/node@22.19.19)(typescript@5.9.3) - postcss-loader@8.1.1(postcss@8.5.12)(typescript@5.8.3)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + postcss-loader@8.1.1(postcss@8.5.12)(typescript@5.8.3)(webpack@5.105.0(postcss@8.5.12)): dependencies: cosmiconfig: 9.0.1(typescript@5.8.3) jiti: 1.21.7 @@ -20863,7 +20866,7 @@ snapshots: safer-buffer@2.1.2: {} - sass-loader@16.0.5(sass@1.85.0)(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + sass-loader@16.0.5(sass@1.85.0)(webpack@5.105.0(postcss@8.5.12)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -21241,7 +21244,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + source-map-loader@5.0.0(webpack@5.105.0(postcss@8.5.12)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -22344,7 +22347,7 @@ snapshots: webpack-sources@3.4.1: {} - webpack-subresource-integrity@5.1.0(webpack@5.105.0(esbuild@0.28.0)(postcss@8.5.12)): + webpack-subresource-integrity@5.1.0(webpack@5.105.0(postcss@8.5.12)): dependencies: typed-assert: 1.0.9 webpack: 5.105.0(esbuild@0.28.0)(postcss@8.5.12) diff --git a/shared/package.json b/shared/package.json index bc6e34b..d346909 100644 --- a/shared/package.json +++ b/shared/package.json @@ -2,6 +2,9 @@ "name": "@datacat/shared", "version": "0.1.0", "private": true, - "main": "src/index.ts", - "types": "src/index.ts" + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc --project tsconfig.build.json" + } } diff --git a/shared/src/schemas/api-entry.schema.ts b/shared/src/schemas/api-entry.schema.ts index e090a80..762be48 100644 --- a/shared/src/schemas/api-entry.schema.ts +++ b/shared/src/schemas/api-entry.schema.ts @@ -1,19 +1,3 @@ -// Zod schemas for validation (used in backend) -// Install zod in back/ if needed: pnpm add zod - export const API_TYPES = ['ASYNCAPI', 'OPENAPI'] as const; export const API_STATUSES = ['PENDING', 'GENERATED', 'ERROR'] as const; - -export interface CreateApiEntryDto { - title: string; - version: string; - description?: string; - type: 'ASYNCAPI' | 'OPENAPI'; -} - -export interface UpdateApiEntryDto { - title?: string; - version?: string; - description?: string; - categoryId?: string | null; -} +export const API_CONVENTIONS = ['CONSULTER', 'ENREGISTRER', 'ETRE_NOTIFIE'] as const; diff --git a/shared/src/types/api-entry.types.ts b/shared/src/types/api-entry.types.ts index 332ef2d..ad37ad8 100644 --- a/shared/src/types/api-entry.types.ts +++ b/shared/src/types/api-entry.types.ts @@ -2,23 +2,37 @@ export type ApiType = 'ASYNCAPI' | 'OPENAPI'; export type ApiStatus = 'PENDING' | 'GENERATED' | 'ERROR'; +export type ApiConvention = 'CONSULTER' | 'ENREGISTRER' | 'ETRE_NOTIFIE'; + +export const API_CONVENTION_LABELS: Record = { + CONSULTER: 'Consulter', + ENREGISTRER: 'Enregistrer', + ETRE_NOTIFIE: 'Être notifié', +}; + export interface ApiEntry { id: string; title: string; + name: string; + convention: ApiConvention; version: string; + versionMajor: number; + versionMinor: number; + versionPatch: number; description: string | null; type: ApiType; + provider: string; yamlContent: string; htmlPath: string | null; status: ApiStatus; errorMessage: string | null; categoryId: string | null; - functionalDoc: string | null; - technicalDoc: string | null; - externalDocUrl: string | null; - contactFunctional: string | null; - contactTechnical: string | null; - accessRights: string | null; + contactFunctionalName: string; + contactFunctionalEntity: string; + contactFunctionalEmail: string; + contactTechnicalName: string; + contactTechnicalEntity: string; + contactTechnicalEmail: string; createdAt: string; updatedAt: string; } @@ -26,9 +40,12 @@ export interface ApiEntry { export interface ApiEntryListItem { id: string; title: string; + name: string; + convention: ApiConvention; version: string; description: string | null; type: ApiType; + provider: string; status: ApiStatus; categoryId: string | null; createdAt: string; diff --git a/shared/tsconfig.build.json b/shared/tsconfig.build.json new file mode 100644 index 0000000..1c53dcd --- /dev/null +++ b/shared/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "target": "ES2021", + "module": "CommonJS", + "moduleResolution": "node", + "outDir": "./dist", + "declaration": true, + "declarationMap": true + }, + "include": ["src/**/*.ts"] +}