import { AadHttpClient, HttpClientResponse } from '@microsoft/sp-http'; // Ответ бэкенда всегда в конверте { data: T } на успехе (Api/Common/ApiResponse.cs) // и { messages: string[] } на ошибке (ApiError). 204 No Content возвращается без тела. interface IApiEnvelope { 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; export interface IHttpClient { get(path: string, query?: QueryParams): Promise; post(path: string, body?: unknown): Promise; put(path: string, body?: unknown): Promise; patch(path: string, body?: unknown): Promise; delete(path: string): Promise; } 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; } async function parseResponse(response: IMinimalResponse): Promise { if (response.status === 204) { return undefined as T; } const text = await response.text(); let json: IApiEnvelope | 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(path: string, query?: QueryParams): Promise { const url = buildUrl(this.baseUrl, path, query); return this.client .get(url, AadHttpClient.configurations.v1) .then((r: HttpClientResponse) => parseResponse(r)); } public post(path: string, body?: unknown): Promise { return this.send('POST', path, body); } public put(path: string, body?: unknown): Promise { return this.send('PUT', path, body); } public patch(path: string, body?: unknown): Promise { return this.send('PATCH', path, body); } public async delete(path: string): Promise { await this.send('DELETE', path); } private send(method: string, path: string, body?: unknown): Promise { 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(r)); } }