Files
basic-erp/apps/api/src/sap/sap-client.service.ts
T

94 lines
3.4 KiB
TypeScript
Raw Normal View History

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<string, string | number | boolean | undefined>;
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<string, string>;
}
export interface SapResponse<T = unknown> {
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<T = unknown>(req: SapRequest): Promise<SapResponse<T>> {
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<T = unknown>(path: string, query?: SapQuery, headers?: Record<string, string>) {
return this.request<T>({ 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('&')}` : '';
}