import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { firstValueFrom } from 'rxjs'; /** Results per page of every paginated Hiboutik endpoint (see "Results per page" in the API docs). */ const HIBOUTIK_PAGE_SIZE = 250; /** Low-level HTTP client for the Hiboutik REST API: base URL and Basic auth credentials come from the environment. */ @Injectable() export class HiboutikClient { private readonly baseUrl: string; private readonly authHeader: string; constructor( private readonly httpService: HttpService, private readonly configService: ConfigService, ) { const username = this.configService.get('HIBOUTIK_USERNAME') ?? ''; const password = this.configService.get('HIBOUTIK_PASSWORD') ?? ''; this.authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`; this.baseUrl = this.configService.get('HIBOUTIK_BASE_URL') ?? ''; } /** GET on a Hiboutik path (relative to `HIBOUTIK_BASE_URL`), returns the response body. */ async get(path: string, params?: Record): Promise { const response = await firstValueFrom( this.httpService.get(`${this.baseUrl}${path}`, { headers: { Authorization: this.authHeader }, params, }), ); return response.data; } /** * GET on a list endpoint. HTTP errors (4xx/5xx) throw, a 2xx answer without body is an empty list, and any other * non-list body throws: an empty list triggers deletions in the sync, so it must never be inferred from a malformed answer. */ async getList(path: string, params?: Record): Promise { const data = await this.get(path, params); if (data === '' || data == null) return []; if (!Array.isArray(data)) throw new Error(`Unexpected answer from Hiboutik ${path}: a list was expected`); return data; } /** * Fetches every page of a paginated endpoint (`p` param, starting at 1) and concatenates the results. * Only for paginated endpoints: on a non-paginated one, a full first page would loop forever on the same results. */ async getAllPages(path: string, baseParams: Record = {}): Promise { const results: T[] = []; let page = 1; while (true) { const pageData = await this.getList(path, { ...baseParams, p: page }); if (pageData.length === 0) break; results.push(...pageData); // A page smaller than the page size is the last one: no need to request an empty page if (pageData.length < HIBOUTIK_PAGE_SIZE) break; page++; } return results; } }