/** * 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 = {}; 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> = {}; for (const op of API_CONTRACT) { const responses: Record = {}; 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 n’est 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)`);