import { Inject, Injectable, Logger } from '@nestjs/common'; import { type Dispatcher, request } from 'undici'; import { AppConfigService } from '../config/app-config.service.js'; import { SAP_DISPATCHER } from './sap.constants.js'; import { describeConnectionError, toSapException } from './sap-errors.js'; import { type SapSession, SapSessionService } from './sap-session.service.js'; export type SapQuery = Record; export interface SapRequest { method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; /** Ruta relativa al Service Layer, ej. `Items('A001')` o `Orders`. */ path: string; query?: SapQuery; body?: unknown; /** Cabeceras B1S-* u otras que decide quien llama. */ headers?: Record; } export interface SapResponse { status: number; headers: Dispatcher.ResponseData['headers']; data: T; } /** Proxy centralizado hacia el Service Layer con reautenticación transparente. */ @Injectable() export class SapClientService { private readonly logger = new Logger(SapClientService.name); constructor( private readonly config: AppConfigService, private readonly sessions: SapSessionService, @Inject(SAP_DISPATCHER) private readonly dispatcher: Dispatcher, ) {} async request(req: SapRequest): Promise> { let session = await this.sessions.getSession(); let res = await this.send(session, req); if (res.status === 401) { this.logger.warn(`Sesión SAP expirada (401) en ${req.method} ${req.path}; reautenticando`); session = await this.sessions.renew(session); res = await this.send(session, req); } if (res.status >= 400) throw toSapException(res.status, res.text); return { status: res.status, headers: res.headers, data: (res.text ? JSON.parse(res.text) : undefined) as T, }; } get(path: string, query?: SapQuery, headers?: Record) { return this.request({ method: 'GET', path, query, headers }); } private async send(session: SapSession, req: SapRequest) { const timeout = this.config.get('SAPB1_SL_TIMEOUT_MS'); const url = `${this.config.get('SAPB1_SL_URL')}/${req.path}${buildQuery(req.query)}`; const started = Date.now(); try { const res = await request(url, { method: req.method, dispatcher: this.dispatcher, headers: { 'content-type': 'application/json', accept: 'application/json', ...req.headers, cookie: session.cookie, }, body: req.body === undefined ? undefined : JSON.stringify(req.body), headersTimeout: timeout, bodyTimeout: timeout, }); const text = await res.body.text(); this.logger.debug(`${req.method} ${req.path} -> ${res.statusCode} (${Date.now() - started} ms)`); return { status: res.statusCode, headers: res.headers, text }; } catch (err) { this.logger.error(`${req.method} ${req.path}: ${(err as Error).message}`); throw describeConnectionError(err); } } } /** Codifica los valores (no las claves `$filter`, `$top`...) con %20 para espacios. */ export function buildQuery(query?: SapQuery): string { if (!query) return ''; const parts = Object.entries(query) .filter(([, v]) => v !== undefined && v !== '') .map(([k, v]) => `${k}=${encodeURIComponent(String(v))}`); return parts.length ? `?${parts.join('&')}` : ''; }