import { HTTPClient, HTTPClientFactory } from "../../../utils/http-client.js"; import { CoreConfiguration } from "../../data-sync-service/configuration-manager.js"; import { QueryParams, buildLogicalCondition, Pagination, } from "./get-query-params.js"; import { ErrorHandler } from "../errors.js"; import { APIResponse, ERPApiConfig } from "../types.js"; const DEFAULT_PAGINATION_LIMIT = 100; // Reasonable default for most APIs /** * RestAPIService is a utility class for making REST API requests to an ERP system. * Provides methods for GET, PATCH, and POST requests, with built-in pagination support. * * @param config - Configuration for the ERP API connection * @param retryAttempts - Optional number of retry attempts for failed requests. Defaults to 0 (no retries) * @param paginationLimit - Optional default limit for paginated requests. Defaults to 100 items per page * * NOTE: assumes bearertoken-based authentication. */ export class RestAPIService { private client: HTTPClient; private config: ERPApiConfig; private paginationLimit: number; constructor( config: ERPApiConfig, retryAttempts?: number, paginationLimit?: number, timeoutSeconds?: number ) { this.config = config; this.paginationLimit = paginationLimit ?? DEFAULT_PAGINATION_LIMIT; const effectiveRetryAttempts = retryAttempts ?? this.config.retryAttempts ?? CoreConfiguration.inst().erpApiRetryAttempts; const effectiveTimeoutSeconds = timeoutSeconds ?? this.config.timeoutSeconds ?? CoreConfiguration.inst().erpApiTimeoutSeconds; this.client = HTTPClientFactory.getInstance({ baseUrl: config.apiUrl, retryAttempts: effectiveRetryAttempts, timeoutMs: effectiveTimeoutSeconds * 1000, }); } //#region Get Request Helpers private async getHeaders(): Promise> { const token = await this.config.getAuthToken(); return { "Content-Type": "application/json", Accept: "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}), }; } private buildQueryParams(query: QueryParams = {}): URLSearchParams { const { limit, nextPage, filter } = query; const queryParams: Record = {}; if (limit) { queryParams.take = limit.toString(); } if (nextPage) { queryParams.skip = nextPage; } if (filter) { if (filter.in) { filter.in.forEach((filterIn) => { const query = filterIn.value.join("|"); queryParams[`${filterIn.field}[in]`] = query; }); } if (filter.and) { queryParams["filter[and]"] = buildLogicalCondition(filter.and, "and"); } if (filter.or) { queryParams["filter[or]"] = buildLogicalCondition(filter.or, "or"); } } return new URLSearchParams(queryParams); } /** * Builds pagination metadata for API responses * @param limit - Optional limit for this specific request. If not provided, uses the service's default pagination limit * @param nextPage - Optional next page token * @param previousPage - Optional previous page token * @param dataLength - Number of items in the current response * @param totalCount - Total number of items available * @returns Pagination metadata including count, limit, and page tokens */ private buildPagination( limit?: number, nextPage?: string, previousPage?: string, dataLength: number = 0, totalCount?: number ): Pagination { const paging: Pagination = { count: totalCount || 0, limit: limit || this.paginationLimit, nextPage: undefined, previousPage, }; if (limit) { paging.limit = limit; const currentSkip = parseInt(nextPage || "0"); // Only set nextPage if: // 1. We have data in this response // 2. We haven't reached the end (current skip + limit < total count) if (dataLength > 0 && (!totalCount || currentSkip + limit < totalCount)) { paging.nextPage = `${currentSkip + limit}`; } } return paging; } private buildUrl(endpoint: string, queryParams?: URLSearchParams): string { const pathParts = [this.config.apiUrl || ""]; pathParts.push(endpoint); const path = pathParts.filter(Boolean).join("/"); return queryParams ? `${path}?${queryParams.toString()}` : path; } //#endregion get request helpers async get( endpoint: string, query: QueryParams = {} ): Promise> { try { const params = this.buildQueryParams(query); const url = this.buildUrl(endpoint, params); const headers = await this.getHeaders(); const response = await this.client.request<{ Data: T[]; TotalCount: number; }>({ method: "GET", url, headers, }); const paging = this.buildPagination( query.limit, query.nextPage, query.previousPage, response.data.Data?.length, response.data.TotalCount ); return { data: response.data.Data || [], metadata: { pagination: paging, }, }; } catch (error) { ErrorHandler.handle(error); } } async patch( endpoint: string, id: string, data: Partial ): Promise> { try { const url = this.buildUrl(`${endpoint}/${id}`); const headers = await this.getHeaders(); const response = await this.client.request({ method: "PATCH", url, data, headers, }); return { data: response.data, }; } catch (error) { ErrorHandler.handle(error); } } async post( endpoint: string, data: TRequest ): Promise> { try { const url = this.buildUrl(endpoint); const headers = await this.getHeaders(); const response = await this.client.request({ method: "POST", url, data, headers, }); return { data: response.data, }; } catch (error) { ErrorHandler.handle(error); } } /** * Cleanup all HTTP connections and resources * Call this when the service is no longer needed */ async destroy(): Promise { await this.client.destroy(); } }