Compare commits

..

2 Commits

Author SHA1 Message Date
z3n
b975e01e15 fix(uploads): bundling AsyncAPI multi-fichiers sans appel réseau
- Ajout @asyncapi/bundler@0.9.0 en dépendance locale
- Remplace execFile('asyncapi bundle') par l'API programmatique
- redocly bundle conservé pour OpenAPI (pas de pb réseau)

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-01 09:08:57 +00:00
z3n
8d6deb8691 fix(uploads): compatibilité Windows pour l'upload multi-fichiers
- Remplace /tmp/uploads par os.tmpdir() (cross-platform)
- Ajoute node_modules/.bin au PATH des commandes bundle asyncapi/redocly
- Supprime la déclaration en double de execFileAsync

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-05-29 14:45:39 +00:00
2 changed files with 23 additions and 10 deletions

View File

@@ -29,6 +29,7 @@
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"class-transformer": "^0.5.0", "class-transformer": "^0.5.0",
"@asyncapi/cli": "6.0.0", "@asyncapi/cli": "6.0.0",
"@asyncapi/bundler": "0.9.0",
"@asyncapi/generator": "3.0.1", "@asyncapi/generator": "3.0.1",
"@asyncapi/html-template": "3.5.6", "@asyncapi/html-template": "3.5.6",
"@redocly/cli": "latest" "@redocly/cli": "latest"

View File

@@ -11,15 +11,23 @@ import { diskStorage } from 'multer';
import * as fs from 'fs'; import * as fs from 'fs';
import * as fsPromises from 'fs/promises'; import * as fsPromises from 'fs/promises';
import * as path from 'path'; import * as path from 'path';
import * as os from 'os';
import { execFile } from 'child_process'; import { execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-require-imports
const asyncapiBundle = require('@asyncapi/bundler');
const execFileAsync = promisify(execFile);
const localBin = path.join(process.cwd(), 'node_modules', '.bin');
const execEnv = {
...process.env,
PATH: `${localBin}${path.delimiter}${process.env.PATH ?? ''}`,
};
import { ApisService } from '../apis/apis.service'; import { ApisService } from '../apis/apis.service';
import { DocsGenerationService } from '../docs-generation/docs-generation.service'; import { DocsGenerationService } from '../docs-generation/docs-generation.service';
import { ApiType } from '@datacat/shared'; import { ApiType } from '@datacat/shared';
const execFileAsync = promisify(execFile);
@Controller('uploads') @Controller('uploads')
export class UploadsController { export class UploadsController {
constructor( constructor(
@@ -30,7 +38,7 @@ export class UploadsController {
@Post() @Post()
@UseInterceptors( @UseInterceptors(
FilesInterceptor('files', 20, { FilesInterceptor('files', 20, {
storage: diskStorage({ destination: '/tmp/uploads' }), storage: diskStorage({ destination: path.join(os.tmpdir(), 'datacat-uploads') }),
fileFilter: (_req, file, cb) => { fileFilter: (_req, file, cb) => {
if (!file.originalname.match(/\.(yaml|yml)$/i)) { if (!file.originalname.match(/\.(yaml|yml)$/i)) {
return cb(new BadRequestException('Only YAML files are allowed'), false); return cb(new BadRequestException('Only YAML files are allowed'), false);
@@ -61,8 +69,9 @@ export class UploadsController {
if (!body.version) throw new BadRequestException('version is required'); if (!body.version) throw new BadRequestException('version is required');
/* Crée le répertoire si absent */ /* Crée le répertoire si absent */
if (!fs.existsSync('/tmp/uploads')) { const uploadDir = path.join(os.tmpdir(), 'datacat-uploads');
fs.mkdirSync('/tmp/uploads', { recursive: true }); if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
} }
let yamlContent: string; let yamlContent: string;
@@ -131,7 +140,7 @@ export class UploadsController {
mainFile: Express.Multer.File, mainFile: Express.Multer.File,
type: ApiType, type: ApiType,
): Promise<string> { ): Promise<string> {
const tempDir = `/tmp/uploads/${randomUUID()}`; const tempDir = path.join(os.tmpdir(), 'datacat-uploads', randomUUID());
const bundledPath = path.join(tempDir, 'bundled.yaml'); const bundledPath = path.join(tempDir, 'bundled.yaml');
try { try {
@@ -145,12 +154,15 @@ export class UploadsController {
const mainPath = path.join(tempDir, mainFile.originalname); const mainPath = path.join(tempDir, mainFile.originalname);
if (type === 'OPENAPI') { if (type === 'OPENAPI') {
await execFileAsync('redocly', ['bundle', mainPath, '--output', bundledPath]); /* redocly bundle : pas d'appel réseau, fonctionne en local */
} else { await execFileAsync('redocly', ['bundle', mainPath, '--output', bundledPath], { shell: true, env: execEnv });
await execFileAsync('asyncapi', ['bundle', mainPath, '--output', bundledPath]);
}
return await fsPromises.readFile(bundledPath, 'utf-8'); return await fsPromises.readFile(bundledPath, 'utf-8');
} else {
/* @asyncapi/bundler API programmatique : aucun appel réseau */
const bundleFn = asyncapiBundle.default ?? asyncapiBundle;
const document = await bundleFn([mainPath], { base: mainPath });
return document.string();
}
} finally { } finally {
/* Nettoyage du répertoire temp même en cas d'erreur */ /* Nettoyage du répertoire temp même en cas d'erreur */
await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => undefined); await fsPromises.rm(tempDir, { recursive: true, force: true }).catch(() => undefined);