Files
siop2/packages/shared/scripts/generate-openapi.ts
pr-daaif 2ffdd13cf4 feat(r3.2): bibliothèque de documents (FileStorage réel) + analytics
- FileStorage : putObject/getObjectStream/removeObject, bucket créé au
  démarrage — MinIO confiné à son implémentation (règle ESLint intacte)
- documents : upload multipart (PDF/JPG/PNG, 20 Mo max, rattachement
  appareil OU OT requis, permission d'édition sur la CIBLE), liste
  filtrable, téléchargement STREAMÉ par l'API (MinIO jamais exposé),
  suppression ; e2e : octets téléchargés identiques aux octets envoyés
- analytics : GET /analytics/summary dérivé du réel — coûts/mois
  (mouvements + main-d'œuvre figés), pannes par organe (bilans codés),
  taux de préventif, durée moyenne de résolution, top équipements
- générateur OpenAPI : query params, multipart, réponse binaire (71 ops)
- CI : service MinIO (bitnami) sur les jobs api et e2e
- test de régression du tri « Interventions récentes » rendu déterministe
  (positions absolues instables sous 12 suites parallèles) ; 58 tests,
  6 runs complets consécutifs verts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 21:24:04 +01:00

130 lines
4.0 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Règle d'or (ADR-001) : Zod → OpenAPI → clients typés.
* Génère docs/openapi.json (COMMITTÉE) depuis src/contract.ts.
* Usage : pnpm --filter @siop/shared contract (raccourci racine : pnpm contract)
* La CI (ci-contract, R0.12) échoue si le fichier committé diffère de la génération.
*/
import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { z } from 'zod';
import { API_CONTRACT } from '../src/contract';
const OUT = resolve(__dirname, '../../../docs/openapi.json');
const schemas: Record<string, unknown> = {};
function register(name: string, schema: z.ZodType): { $ref: string } {
if (!(name in schemas)) {
schemas[name] = z.toJSONSchema(schema, { target: 'draft-2020-12' });
}
return { $ref: `#/components/schemas/${name}` };
}
const paths: Record<string, Record<string, unknown>> = {};
for (const op of API_CONTRACT) {
const responses: Record<string, unknown> = {};
for (const [status, res] of Object.entries(op.responses)) {
responses[status] = {
description: res.description,
...(res.schema && res.name
? { content: { 'application/json': { schema: register(res.name, res.schema) } } }
: {}),
};
}
paths[op.path] = {
...(paths[op.path] ?? {}),
[op.method]: {
operationId: op.operationId,
summary: op.summary,
tags: op.tags,
...(op.pathParams?.length || op.queryParams?.length
? {
parameters: [
...(op.pathParams ?? []).map((name) => ({
name,
in: 'path',
required: true,
// Convention : « id » / «…Id » sont des UUID, le reste est libre
schema:
name === 'id' || name.endsWith('Id')
? { type: 'string', format: 'uuid' }
: { type: 'string' },
})),
...(op.queryParams ?? []).map((q) => ({
name: q.name,
in: 'query',
required: q.required ?? false,
schema: { type: 'string' },
})),
],
}
: {}),
...(op.isPublic ? {} : { security: [{ bearerAuth: [] }] }),
...(op.demoOnly
? {
'x-demo-only': true,
description:
'ADR-002 : cette route est absente (404) quand DEMO_MODE nest pas actif.',
}
: {}),
...(op.request
? {
requestBody: {
required: true,
content: {
'application/json': { schema: register(op.request.name, op.request.schema) },
},
},
}
: {}),
...(op.multipartFields
? {
requestBody: {
required: true,
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: Object.fromEntries(
Object.entries(op.multipartFields).map(([name, sorte]) => [
name,
sorte === 'file'
? { type: 'string', format: 'binary' }
: { type: 'string' },
]),
),
required: ['file'],
},
},
},
},
}
: {}),
responses,
},
};
}
const doc = {
openapi: '3.1.0',
info: {
title: 'SIOP V2 API',
version: '0.1.0',
description:
'GMAO ascenseurs — contrat R0 (auth, démo-login ADR-002, profil, santé). ' +
'Généré depuis packages/shared/src/contract.ts — NE PAS ÉDITER À LA MAIN.',
},
paths,
components: {
schemas,
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
},
},
};
mkdirSync(dirname(OUT), { recursive: true });
writeFileSync(OUT, JSON.stringify(doc, null, 2) + '\n');
console.log(`OpenAPI générée : ${OUT} (${API_CONTRACT.length} opérations)`);