import { EventEmitter } from "events"; import { type TokenStorageStrategy } from "./utils/TokenStorage.js"; /** * Résultat retourné quand une requête est **mise en file** (offline ou breaker). * Permet de typer précisément les chemins « non-réseau » de `callEndpoint`. */ export interface ApiClientOfflineEnqueueResult { data: null; offline?: true; breaker?: true; } /** * Options de configuration pour ApiClient */ export interface ApiClientOptions { baseURL: string; accessToken?: string | null; refreshToken?: string | null; refreshUrl?: string; endpoints?: any[]; timeout?: number; debug?: boolean; maxRetries?: number; circuitBreakerThreshold?: number; circuitBreakerResetTime?: number; fromJSONValue?: boolean; /** * Mode de dialogue avec le backend cocolight-backend : * - `legacy` (défaut) : réponses byte-compatibles PHP ; la lib applique TOUTES ses * normalisations côté client (_transformData/_normalizeJsonData) + validation AJV. * - `clean` : envoie `X-Coco-Mode: clean` ; le SERVEUR applique les normalisations et * répond {success, data|error}. La lib unwrap l'enveloppe, lève ApiResponseError sur * success:false, SAUTE _transformData et la validation AJV (schémas legacy), et revive * (dates ISO -> Date, id -> _id ObjectID) pour rendre à l'app le MÊME shape qu'avant. * NB : l'intercepteur refresh-token reste en legacy (appel interne sans le header). */ mode?: "legacy" | "clean"; tokenStorageStrategy?: TokenStorageStrategy | null; } /** * Structure des données pour les appels d'endpoint */ export interface CallEndpointData { pathParams?: Record; [key: string]: any; } /** * Résultat d'un appel d'endpoint réussi */ export interface CallEndpointResult { data: T; offline?: never; breaker?: never; } /** * Union type pour tous les résultats possibles de callEndpoint */ export type CallEndpointResponse = CallEndpointResult | ApiClientOfflineEnqueueResult; /** * Client générique pour consommer une API REST avec validation AJV, gestion des tokens, * circuit breaker, retry automatique, et support offline. * * @fires ApiClient#retryAttempt * @fires ApiClient#queuedOffline * @fires ApiClient#circuitBreakerOpen * @fires ApiClient#circuitBreakerReset * @fires ApiClient#refreshSuccess * @fires ApiClient#refreshFailed * @fires ApiClient#sessionReset * @fires ApiClient#validationError * @fires ApiClient#offlineModeChanged * @fires ApiClient#userLoggedIn */ export default class ApiClient extends EventEmitter { readonly __entityTag: string; readonly userId: string | null; private readonly _baseURL; private readonly _refreshUrl; private readonly _endpoints; /** Index constant -> endpoint, construit une fois (évite un .find() O(n) sur ~141 endpoints par appel). */ private readonly _endpointsByConstant; private readonly _debug; private readonly _fromJSONValue; private readonly _tokenStorage; private _offlineClientManager; private _ajv; private _ajvNoDefaults; _logger: any; private _client; private readonly _breakerThreshold; private readonly _breakerResetTime; private _breakerErrorCount; private _breakerOpen; private _lastBreakerOpenTime; private _accessToken; private _refreshToken; private _setUserId; private _mode; constructor({ baseURL, accessToken, refreshToken, refreshUrl, endpoints, timeout, debug, maxRetries, circuitBreakerThreshold, circuitBreakerResetTime, fromJSONValue, mode, tokenStorageStrategy }: ApiClientOptions); /** * Sets the access token for the API client and updates the authorization header. */ setToken(token: string | null): void; /** * Retrieves the current access token. */ getToken(): string | null; /** * Sets the refresh token for the API client. */ setRefreshToken(token: string | null): void; /** * Retrieves the current refresh token. */ getRefreshToken(): string | null; /** * Indique si le client est connecté. * On considère que le client est connecté si un token d'accès (_accessToken) est défini. */ get isConnected(): boolean; /** * Extrait l'identifiant depuis un JWT. */ private _getIdFromToken; /** Promesse de refresh en cours, partagée par toutes les requêtes 401 concurrentes (single-flight). */ private _refreshPromise; /** * Méthode simplifiée de refresh (en JSON). * Single-flight : un seul appel réseau /refresh à la fois. Les 401 concurrents * attendent la même promesse au lieu de déclencher N refresh en parallèle — ce qui, * avec un refresh token à usage unique/rotatif, faisait échouer toutes les rafales * sauf la première et détruisait la session via resetSession(). * Emet un event refreshSuccess si ça marche. */ private _refreshAccessToken; private _performTokenRefresh; /** * checkCircuitBreaker : vérifie si on peut appeler l'API ou non * si le breaker est "open", on regarde si on peut "reset" */ private _checkCircuitBreaker; /** * updateCircuitBreaker : incremente le compteur d'erreurs * si on dépasse le threshold, on "open" le breaker. */ private _updateCircuitBreakerError; /** * resetCircuitBreaker : en cas de succès on reset le compteur */ private _resetCircuitBreakerSuccess; static stripNullsInPlace(obj: any): any; private _resolveSpecialValuesInPlace; private _cleanSchemaLeftoverAlias; /** * Safely calls an asynchronous function and handles any errors that occur. * Logs the error message using the instance's logger before re-throwing the error. * * @param fn - The asynchronous function to be called. * @param args - The arguments to pass to the function. * @returns The result of the asynchronous function. * @throws {Error} Re-throws any error that occurs during the function execution. */ safeCall(fn: (...args: any[]) => Promise, ...args: any[]): Promise; /** * Appelle un endpoint avec validations AJV, auth et circuit breaker. * En cas d’indisponibilité (offline ou breaker), **met en file** l’action via * `_offlineClientManager` si présent. * * @param constant - The constant representing the endpoint to call. * @param data - Données envoyées (incluant éventuellement `pathParams`). * @param transformResponseData - `true` (transforme via `_transformData`), `false`, ou une fonction `(data) => any`. * @param validateResponseSchema - Whether to validate the response schema. * @returns The response from the endpoint call, or an enqueue result if offline or breaker is active. * @throws {CircuitBreakerError} If the circuit breaker is activated. * @throws {ApiClientError} If the endpoint is not found, token is required but not provided, or validation fails. */ callEndpoint(constant: string, data?: CallEndpointData, transformResponseData?: boolean | ((data: any) => any), validateResponseSchema?: boolean): Promise>; /** * Converts AJV (Another JSON Schema Validator) errors into human-readable messages. * * @param errors - An array of AJV validation error objects. * @returns An array of human-readable error messages extracted from the AJV errors. */ private _ajvErrorHuman; /** * Checks the API response for errors and throws an `ApiResponseError` if any are found. * * This method examines the `response.data` object to determine if it contains * error information. It handles both standard and nested error cases: * * - Standard case: If `data.result` is a boolean and is `false`, an error is thrown. * - Nested case: If `data.resultErrors` exists and contains a `result` property * that is a boolean and is `false`, an error is thrown. * * If no errors are found, the original `response` is returned. * * @param {import("axios").AxiosResponse | { data: any } } response * @returns {import("axios").AxiosResponse | { data: any }} * @throws {ApiResponseError} If an error is detected in the response data. */ checkAndThrowApiResponseError(response: any): any; /** * Réinitialise complètement la session de l'utilisateur : * - Token d'accès * - Token de rafraîchissement * - userId * - En-têtes Axios */ resetSession(): void; /** * Retrieves the value from an object based on a dot-separated path. */ private _getValueByPath; /** * Normalizes Date objects to ISO strings for AJV validation. * This preserves the object structure while converting only Dates. * Handles reactive Proxies by unwrapping them first. * * @param obj - The object to normalize * @returns The normalized object with Dates as ISO strings */ static normalizeDatesForValidation(obj: any): any; /** * Normalizes fields with format:"date" in schema to YYYY-MM-DD format. * Extracts only the date part from ISO datetime strings. * * @param data - The data object to normalize * @param schema - The JSON schema containing field definitions * @returns The normalized data with date-only strings for format:"date" fields */ static normalizeSchemaDateFields(data: any, schema: any): any; /** * Converts an object to URL search parameters. * * @param obj - The object to be converted to URL search parameters. * @param [options={}] - Optional settings for the conversion. * @returns The URL search parameters generated from the object. */ static toURLSearchParams(obj: any, options?: any): URLSearchParams; /** * Builds parameters for an API request from a given object. */ static _buildParams(obj: any, paramsInstance: FormData | URLSearchParams, options?: { dots?: boolean; indexes?: boolean; metaTokens?: boolean; }): FormData | URLSearchParams; /** * Transforms the given data based on its structure. * * This method normalizes various structures of data into a consistent format. * It handles different types of data including objects, arrays, and specific * nested structures like `resultGoods`, `resultErrors`, `results`, `news`, * `notif`, `citoyens`, `organizations`, `cities`, `newComment`, `map`, and `object`. * * @param data - The data to be transformed. * @returns - The transformed data. * * @private */ /** * Reviver du mode clean : redonne à l'app le shape exact qu'elle recevait après les * normalisations legacy de la lib — dates ISO (champs _dateFields) -> instances Date, * `id` oid-hex -> `_id` instance ObjectID (créée via EJSON.fromJSONValue, MÊME chemin * que le mode legacy). Le serveur clean a déjà fait tout le reste. */ private _reviveClean; private _transformData; /** * Normalizes JSON data by transforming specific fields and ensuring URLs are complete. * * @param item - The JSON object to be normalized. * @returns - The normalized JSON object. * * The function performs the following transformations: * - Normalizes the ID field if it matches a specific pattern. * - Converts date fields from seconds to milliseconds. * - Ensures URLs in specific fields are complete. * - Filters and normalizes nested objects and arrays. * - Removes the `timeAgo` field if it exists. * - Converts the object to EJSON format if necessary. */ private _normalizeJsonData; /** * Ensures that the given image path is a full URL. If the image path is already a full URL * (i.e., it starts with "http://" or "https://"), it is returned as is. Otherwise, the image path * is concatenated with the base URL of the ApiClient instance. * * @param imagePath - The image path to ensure as a full URL. * @returns - The full URL of the image path. */ private _ensureFullURL; /** * Parcourt récursivement un objet pour normaliser les champs de date. * Pour chaque clé présente dans dateFields, si la valeur est un nombre ou un objet * contenant une propriété sec (nombre), la transforme en { $date: valeurEnMs }. * * @param obj - L’objet à normaliser. * @returns L’objet normalisé. */ /** * Parcourt récursivement un objet pour normaliser les chemins d'images. * Pour chaque propriété correspondant à un champ d'image (dans imageFields), * vérifie et convertit le chemin en URL complète à l’aide de _ensureFullURL. * Les cas particuliers (comme mediaImg.images ou mediaFile.files) sont également gérés. * * @param obj - L’objet à normaliser. * @returns L’objet normalisé. */ /** * Parcourt récursivement un objet ou un tableau pour normaliser les identifiants (ID). * Si un objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide, * l'ID est normalisé et la propriété _id est convertie au format EJSON attendu. * * @param obj - L'objet ou le tableau à normaliser. * @returns L'objet ou le tableau normalisé. */ /** * Normalise récursivement les valeurs booléennes. * Si une valeur est une chaîne "true" ou "false", elle est convertie en booléen. * * @param data - L'objet, le tableau ou la valeur à normaliser. * @returns La donnée normalisée. */ /** * Transforme une chaîne "true" ou "false" en booléen. * * @param str - La chaîne à normaliser. * @returns La valeur booléenne ou la chaîne originale. */ private _normalizeString; /** * Normalise les identifiants dans un objet. * Si l'objet possède une propriété "_id" ou "id" contenant un sous-objet "$id" valide, * il met à jour l'objet en assignant la valeur de l'ID et en formattant _id en EJSON. * * @param obj - L'objet à normaliser. * @returns L'objet avec l'ID normalisé. */ private _normalizeId; /** * Normalise une valeur de date. * Si la valeur est un objet contenant "sec" (nombre) ou un nombre, * elle est convertie en { $date: valeurEnMs }. * * @param value - La valeur à normaliser. * @returns La valeur normalisée. */ private _normalizeDate; /** * Normalise une URL d'image. * Si la valeur est une chaîne non vide, retourne l'URL complète via _ensureFullURL. * * @param value - La valeur à normaliser. * @returns La valeur normalisée. */ private _normalizeImage; /** * Normalise les valeurs numériques renvoyées en string par le backend * (cas typique : content-type application/x-www-form-urlencoded qui * sérialise tout en string, et le backend stocke tel quel). * * @param value - La valeur à normaliser. * @returns Number si conversion possible, sinon la valeur d'origine. */ private _normalizeNumber; /** * Liste des champs d'image à normaliser. */ private _imageFields; /** * Liste des champs de date à normaliser. */ private _dateFields; /** * Liste des champs numériques à normaliser (le backend les renvoie en string * via x-www-form-urlencoded ; sans cette coercion, JSON.stringify diff * produit des faux positifs dans `hasChanges()`). */ private _numberFields; /** * Normalise récursivement un objet, un tableau ou une valeur simple. * Cette fonction réalise en une seule passe les opérations suivantes : * - Conversion des chaînes "true"/"false" en booléens. * - Normalisation des dates (pour les champs spécifiés). * - Normalisation des images (pour les champs spécifiés). * - Transformation récursive des objets et tableaux. * - Normalisation des identifiants. * * @param data - La donnée à normaliser. * @returns La donnée normalisée. */ private _normalizeRecursively; private _startBeforeEndValidate; /** * Récupère le schéma de requête pour un endpoint donné. * @param constant - Le nom de l'endpoint. * @returns Le schéma de requête ou null si non trouvé. */ getRequestSchema(constant: string): any; /** * Récupère le schéma de chemin pour un endpoint donné. * @param constant - Le nom de l'endpoint. * @returns Le schéma de chemin ou null si non trouvé. */ getPathSchema(constant: string): any; /** * Permet d'écouter facilement un ensemble d'événements importants émis par l'ApiClient. * * @param handlers - Un objet avec des fonctions à appeler selon l'événement. * @param [handlers.retryAttempt] - Lors d'une tentative de retry axios. * @param [handlers.queuedOffline] - Lorsqu'une requête est mise en file. * @param [handlers.circuitBreakerOpen] - Quand le breaker s'ouvre. * @param [handlers.circuitBreakerReset] - Quand le breaker se referme. * @param [handlers.refreshSuccess] - Quand le token est rafraîchi. * @param [handlers.refreshFailed] - Quand le refresh échoue. * @param [handlers.sessionReset] - Quand la session est réinitialisée. * @param [handlers.validationError] - Quand une validation échoue. * @param [handlers.offlineModeChanged] - Quand le mode offline change. * @param [handlers.userLoggedIn] - Quand un utilisateur se connecte. */ onEvent(handlers?: Record void>): void; /** * Retourne la liste des noms d'événements personnalisés déclarés dans les endpoints. * Utile pour introspection ou documentation. * * @returns Liste des événements émis dynamiquement via postActions. */ getDeclaredEvents(): unknown[]; } /** * @event ApiClient#retryAttempt * @type {Object} * @property {number} retryCount - Le numéro de tentative. * @property {string} url - L'URL de la requête ayant échoué. */ /** * @event ApiClient#queuedOffline * @type {Object} * @property {string} constant - Le nom de l'endpoint. * @property {Object} data - Les données envoyées. * @property {"offlineMode"|"circuitBreaker"} reason - La raison de la mise en file. */ /** * @event ApiClient#circuitBreakerOpen * @type {Object} * @property {number} timestamp - Date (ms) à laquelle le breaker s'est ouvert. */ /** * @event ApiClient#circuitBreakerReset * @type {void} */ /** * @event ApiClient#refreshSuccess * @type {Object} * @property {string} token - Le nouveau token d'accès. * @property {string} [refreshToken] - Le nouveau refreshToken (optionnel). */ /** * @event ApiClient#refreshFailed * @type {Object} * @property {string} error - Le message d’erreur de refresh. */ /** * @event ApiClient#sessionReset * @type {void} */ /** * @event ApiClient#validationError * @type {Object} * @property {"pathParams"|"request"|"response"} stage - Étape de validation échouée. * @property {Array} errors - Erreurs AJV brutes. */ /** * @event ApiClient#offlineModeChanged * @type {boolean} - true si le client est offline, false sinon. */ /** * @event ApiClient#userLoggedIn * @param user - Les données utilisateur extraites de la réponse. */