175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
import * as http from 'http';
|
|
import * as https from 'https';
|
|
import { URL } from 'url';
|
|
import {
|
|
BadGatewayException,
|
|
Injectable,
|
|
Logger,
|
|
RequestTimeoutException,
|
|
} from '@nestjs/common';
|
|
|
|
export type TahesabCallOptions = {
|
|
baseUrl: string;
|
|
token: string;
|
|
dbName: string;
|
|
/** مسیر OpenAPI مثل DoNewMoshtari یا CheckHealth-SSL */
|
|
path: string;
|
|
/** کلید body؛ اگر خالی باشد فقط GET بدون body */
|
|
bodyKey?: string;
|
|
args?: unknown[];
|
|
method?: 'GET' | 'POST';
|
|
allowInsecureTls?: boolean;
|
|
timeoutMs?: number;
|
|
/**
|
|
* اگر تهحساب با فیلد ERROR و متن منطبق جواب داد، بهجای ۵۰۲
|
|
* مقدار خالی ([] ) برگردان — مثل «رکوردی یافت نشد» در DoListAsnad.
|
|
*/
|
|
emptyResultOnError?: RegExp;
|
|
};
|
|
|
|
function extractTahesabError(json: unknown, text: string): string {
|
|
if (json && typeof json === 'object') {
|
|
const o = json as Record<string, unknown>;
|
|
const err = o.ERROR ?? o.Error ?? o.error ?? o.message ?? o.Message;
|
|
if (typeof err === 'string' && err.trim()) return err.trim().slice(0, 300);
|
|
if (Array.isArray(err) && err.length) return String(err[0]).slice(0, 300);
|
|
}
|
|
const trimmed = text.trim();
|
|
return trimmed ? trimmed.slice(0, 300) : '';
|
|
}
|
|
|
|
@Injectable()
|
|
export class TahesabHttpClient {
|
|
private readonly logger = new Logger(TahesabHttpClient.name);
|
|
|
|
async call<T = unknown>(opts: TahesabCallOptions): Promise<T> {
|
|
const base = opts.baseUrl.replace(/\/+$/, '');
|
|
const path = opts.path.replace(/^\/+/, '');
|
|
const url = new URL(`${base}/${path}`);
|
|
const method = opts.method ?? (opts.bodyKey ? 'POST' : 'GET');
|
|
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
|
|
const headers: Record<string, string> = {
|
|
Authorization: `Bearer ${opts.token}`,
|
|
DBname: opts.dbName,
|
|
DBName: opts.dbName,
|
|
Accept: 'application/json',
|
|
Host: url.host,
|
|
};
|
|
|
|
let body: string | undefined;
|
|
if (method === 'POST' && opts.bodyKey) {
|
|
headers['Content-Type'] = 'application/json';
|
|
body = JSON.stringify({ [opts.bodyKey]: opts.args ?? [] });
|
|
headers['Content-Length'] = Buffer.byteLength(body).toString();
|
|
}
|
|
|
|
const isHttps = url.protocol === 'https:';
|
|
const lib = isHttps ? https : http;
|
|
const agent =
|
|
isHttps && opts.allowInsecureTls
|
|
? new https.Agent({ rejectUnauthorized: false })
|
|
: undefined;
|
|
|
|
return new Promise<T>((resolve, reject) => {
|
|
const req = lib.request(
|
|
{
|
|
protocol: url.protocol,
|
|
hostname: url.hostname,
|
|
port: url.port || (isHttps ? 443 : 80),
|
|
path: `${url.pathname}${url.search}`,
|
|
method,
|
|
headers,
|
|
timeout: timeoutMs,
|
|
agent,
|
|
},
|
|
(res) => {
|
|
const chunks: Buffer[] = [];
|
|
res.on('data', (c) => chunks.push(c));
|
|
res.on('end', () => {
|
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
let json: unknown = null;
|
|
if (text) {
|
|
try {
|
|
json = JSON.parse(text);
|
|
} catch {
|
|
json = { raw: text };
|
|
}
|
|
}
|
|
const status = res.statusCode ?? 0;
|
|
const detail = extractTahesabError(json, text);
|
|
const isEmptyOk =
|
|
Boolean(detail) &&
|
|
Boolean(opts.emptyResultOnError) &&
|
|
opts.emptyResultOnError!.test(detail);
|
|
|
|
if (status < 200 || status >= 300) {
|
|
if (isEmptyOk) {
|
|
this.logger.debug(
|
|
`Tahesab ${path} ${status} empty-on-error: ${detail.slice(0, 120)}`,
|
|
);
|
|
resolve([] as T);
|
|
return;
|
|
}
|
|
this.logger.warn(
|
|
`Tahesab ${method} ${path} → ${status}: ${text.slice(0, 400)}`,
|
|
);
|
|
reject(
|
|
new BadGatewayException(
|
|
detail
|
|
? `تهحساب (${status}): ${detail}`
|
|
: `پاسخ نامعتبر تهحساب (${status}) برای ${path}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
// بعضی متدها با ۲۰۰ و فیلد ERROR برمیگردند
|
|
if (json && typeof json === 'object' && 'ERROR' in (json as object)) {
|
|
if (isEmptyOk) {
|
|
this.logger.debug(
|
|
`Tahesab ${path} empty-on-error: ${detail.slice(0, 120)}`,
|
|
);
|
|
resolve([] as T);
|
|
return;
|
|
}
|
|
reject(
|
|
new BadGatewayException(
|
|
detail ? `تهحساب: ${detail}` : `خطای تهحساب برای ${path}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
resolve(json as T);
|
|
});
|
|
},
|
|
);
|
|
|
|
req.on('timeout', () => {
|
|
req.destroy();
|
|
reject(
|
|
new RequestTimeoutException(
|
|
`مهلت اتصال به تهحساب برای ${path} به پایان رسید.`,
|
|
),
|
|
);
|
|
});
|
|
|
|
req.on('error', (err) => {
|
|
this.logger.error(`Tahesab call failed: ${path}`, err);
|
|
const raw = err.message || String(err);
|
|
const tlsHint =
|
|
/self-signed|certificate|UNABLE_TO_VERIFY|CERT_/i.test(raw)
|
|
? ' — در تنظیمات اتصال، گزینه «پذیرش گواهی SSL خودامضا» را فعال کنید.'
|
|
: '';
|
|
reject(
|
|
new BadGatewayException(
|
|
`اتصال به تهحساب ناموفق بود: ${raw}${tlsHint}`,
|
|
),
|
|
);
|
|
});
|
|
|
|
if (body) req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
}
|