Files
tnsu-approval-route/frontend/src/webparts/approvalAdmin/services/httpClient.ts
T
2026-08-14 14:51:48 +05:00

119 lines
4.0 KiB
TypeScript

import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http';
// Ответ бэкенда всегда в конверте { data: T } на успехе (Api/Common/ApiResponse.cs)
// и { messages: string[] } на ошибке (ApiError). 204 No Content возвращается без тела.
interface IApiEnvelope<T> {
data?: T;
messages?: string[];
}
export class ApiClientError extends Error {
public readonly status: number;
public readonly messages: string[];
constructor(status: number, messages: string[]) {
super(messages.join('; ') || `HTTP ${status}`);
this.status = status;
this.messages = messages;
}
}
export type QueryParams = Record<string, string | number | boolean | undefined>;
export interface IHttpClient {
get<T>(path: string, query?: QueryParams): Promise<T>;
post<T>(path: string, body?: unknown): Promise<T>;
put<T>(path: string, body?: unknown): Promise<T>;
patch<T>(path: string, body?: unknown): Promise<T>;
delete(path: string): Promise<void>;
}
function buildUrl(baseUrl: string, path: string, query?: QueryParams): string {
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`;
const url = new URL(path.replace(/^\//, ''), normalizedBase);
if (query) {
Object.keys(query).forEach(key => {
const value = query[key];
if (value !== undefined && value !== '') {
url.searchParams.set(key, String(value));
}
});
}
return url.toString();
}
interface IMinimalResponse {
ok: boolean;
status: number;
text(): Promise<string>;
}
async function parseResponse<T>(response: IMinimalResponse): Promise<T> {
if (response.status === 204) {
return undefined as T;
}
const text = await response.text();
let json: IApiEnvelope<T> | undefined;
try {
json = text ? JSON.parse(text) : undefined;
} catch {
// Ответ пришёл не в конверте ApiResponse/ApiError (например, "сырая" страница
// необработанного исключения ASP.NET Core или ошибка от прокси перед API).
if (!response.ok) {
throw new ApiClientError(response.status, [text || `HTTP ${response.status}`]);
}
throw new ApiClientError(response.status, ['Сервер вернул не-JSON ответ.']);
}
if (!response.ok) {
const messages = json?.messages ?? [`HTTP ${response.status}`];
throw new ApiClientError(response.status, messages);
}
return (json?.data as T) ?? (undefined as T);
}
/**
* Прод/staging: вызовы через AAD-защищённое SPFx-подключение (AadHttpClient).
* Требует webApiPermissionRequests в package-solution.json и админского
* согласия в SharePoint admin center → Advanced → API access.
*/
export class AadApiHttpClient implements IHttpClient {
constructor(private readonly client: AadHttpClient, private readonly baseUrl: string) {}
public get<T>(path: string, query?: QueryParams): Promise<T> {
const url = buildUrl(this.baseUrl, path, query);
return this.client
.get(url, AadHttpClient.configurations.v1)
.then((r: HttpClientResponse) => parseResponse<T>(r));
}
public post<T>(path: string, body?: unknown): Promise<T> {
return this.send<T>('POST', path, body);
}
public put<T>(path: string, body?: unknown): Promise<T> {
return this.send<T>('PUT', path, body);
}
public patch<T>(path: string, body?: unknown): Promise<T> {
return this.send<T>('PATCH', path, body);
}
public async delete(path: string): Promise<void> {
await this.send<void>('DELETE', path);
}
private send<T>(method: string, path: string, body?: unknown): Promise<T> {
const url = buildUrl(this.baseUrl, path);
return this.client
.fetch(url, AadHttpClient.configurations.v1, {
method,
headers: { 'Content-Type': 'application/json' },
body: body !== undefined ? JSON.stringify(body) : undefined
})
.then((r: HttpClientResponse) => parseResponse<T>(r));
}
}