import { ApiRequestDataTypeInterface } from "../interfaces/ApiRequestDataTypeInterface"; import { ApiResponseInterface } from "../interfaces/ApiResponseInterface"; export enum HttpMethod { GET = "GET", POST = "POST", PUT = "PUT", PATCH = "PATCH", DELETE = "DELETE", } export interface NextRef { next?: string; } export interface PreviousRef { previous?: string; } export interface SelfRef { self?: string; } export interface TotalRef { total?: number; } // Store the last total from any API call - accessible by hooks let lastApiTotal: number | undefined = undefined; export function getLastApiTotal(): number | undefined { return lastApiTotal; } export function clearLastApiTotal(): void { lastApiTotal = undefined; } // Store the last full top-level `meta` object from any list API call - accessible by // hooks that need aggregate meta beyond `total` (e.g. per-list counts). Same // module-global design as lastApiTotal above. let lastApiMeta: Record | undefined = undefined; export function getLastApiMeta(): Record | undefined { return lastApiMeta; } export function clearLastApiMeta(): void { lastApiMeta = undefined; } let globalErrorHandler: ((status: number, message: string) => void) | null = null; /** * Set a global error handler for API errors (client-side only). * This handler will be called instead of throwing errors. */ export function setGlobalErrorHandler(handler: (status: number, message: string) => void) { globalErrorHandler = handler; } /** * Get the current global error handler. */ export function getGlobalErrorHandler(): ((status: number, message: string) => void) | null { return globalErrorHandler; } /** * Abstract base class for services that interact with the JSON:API. * Extend this class to create feature-specific services. */ export abstract class AbstractService { /** * Extract locale from client-side URL pathname * URL structure: /{locale}/route-path (e.g., /it/accounts) * Fallback chain: URL locale → navigator.language → "en" */ private static getClientLocale(): string { if (typeof window === "undefined") { return "en"; // Server-side fallback } // Extract locale from URL pathname (first segment after leading slash) const pathSegments = window.location.pathname.split("/").filter(Boolean); const urlLocale = pathSegments[0]; // Validate against supported locales (currently only "en") const supportedLocales = ["en"]; if (urlLocale && supportedLocales.includes(urlLocale)) { return urlLocale; } // Fallback to navigator language const navigatorLocale = navigator.language.split("-")[0]; if (navigatorLocale && supportedLocales.includes(navigatorLocale)) { return navigatorLocale; } // Final fallback return "en"; } static async next(params: { type: ApiRequestDataTypeInterface; endpoint: string; next?: NextRef; previous?: PreviousRef; self?: SelfRef; total?: TotalRef; }): Promise { return await this.callApi({ method: HttpMethod.GET, type: params.type, endpoint: params.endpoint, next: params.next, previous: params.previous, self: params.self, total: params.total, }); } /** * Fetch the previous page of results. */ static async previous(params: { type: ApiRequestDataTypeInterface; endpoint: string; next?: NextRef; previous?: PreviousRef; self?: SelfRef; total?: TotalRef; }): Promise { return await this.callApi({ method: HttpMethod.GET, type: params.type, endpoint: params.endpoint, next: params.next, previous: params.previous, self: params.self, total: params.total, }); } /** * Make an API call with automatic environment detection and error handling. */ protected static async callApi(params: { type: ApiRequestDataTypeInterface; method: HttpMethod; endpoint: string; companyId?: string; input?: any; overridesJsonApiCreation?: boolean; next?: NextRef; previous?: PreviousRef; self?: SelfRef; total?: TotalRef; responseType?: ApiRequestDataTypeInterface; files?: { [key: string]: File | Blob } | File | Blob; token?: string; suppressGlobalError?: boolean; /** * Per-call override of the API base URL. When omitted, the existing global * `NEXT_PUBLIC_API_URL` (or `configureJsonApi({ apiUrl })`) resolution is used — * fully backward compatible. Ignored when `endpoint` starts with "http" (passthrough). */ baseUrl?: string; }): Promise { // Dynamic import to avoid bundling issues const { JsonApiGet, JsonApiPost, JsonApiPut, JsonApiPatch, JsonApiDelete } = await import("../../unified/JsonApiRequest"); let apiResponse: ApiResponseInterface; // Get language based on environment let language = "en"; if (typeof window === "undefined") { const { getLocale } = await import("next-intl/server"); language = (await getLocale()) ?? "en"; } else { // Client-side: extract locale from URL pathname language = this.getClientLocale(); } switch (params.method) { case HttpMethod.GET: apiResponse = await JsonApiGet({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, language: language, token: params.token, baseUrl: params.baseUrl, }); break; case HttpMethod.POST: apiResponse = await JsonApiPost({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, overridesJsonApiCreation: params.overridesJsonApiCreation, language: language, responseType: params.responseType, files: params.files, token: params.token, baseUrl: params.baseUrl, }); break; case HttpMethod.PUT: apiResponse = await JsonApiPut({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, language: language, responseType: params.responseType, files: params.files, token: params.token, baseUrl: params.baseUrl, }); break; case HttpMethod.PATCH: apiResponse = await JsonApiPatch({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, overridesJsonApiCreation: params.overridesJsonApiCreation, language: language, responseType: params.responseType, files: params.files, token: params.token, baseUrl: params.baseUrl, }); break; case HttpMethod.DELETE: apiResponse = await JsonApiDelete({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, language: language, responseType: params.responseType, token: params.token, baseUrl: params.baseUrl, }); break; default: throw new Error("Method not found"); } if (!apiResponse.ok) { if (globalErrorHandler && typeof window !== "undefined" && !params.suppressGlobalError) { globalErrorHandler(apiResponse.response, apiResponse.error); return undefined as any; } else { const error = new Error(`${apiResponse.response}:${apiResponse.error}`) as any; error.status = apiResponse.response; error.digest = `HTTP_${apiResponse.response}`; error.body = apiResponse.raw; throw error; } } if (apiResponse.next && params.next) params.next.next = apiResponse.next; if (apiResponse.prev && params.previous) params.previous.previous = apiResponse.prev; if (apiResponse.self && params.self) params.self.self = apiResponse.self; // Always store total for hooks to access, and also populate ref if provided if (apiResponse.meta?.total !== undefined) { lastApiTotal = apiResponse.meta.total; if (params.total) params.total.total = apiResponse.meta.total; } // Store the full top-level meta too, so hooks can read aggregates beyond `total`. lastApiMeta = apiResponse.meta; return apiResponse.data as T; } /** * Make an API call and return both data and meta from the response. */ protected static async callApiWithMeta(params: { type: ApiRequestDataTypeInterface; method: HttpMethod; endpoint: string; companyId?: string; input?: any; overridesJsonApiCreation?: boolean; responseType?: ApiRequestDataTypeInterface; files?: { [key: string]: File | Blob } | File | Blob; suppressGlobalError?: boolean; }): Promise<{ data: T; meta?: Record }> { // Dynamic import to avoid bundling issues const { JsonApiGet, JsonApiPost, JsonApiPut, JsonApiPatch, JsonApiDelete } = await import("../../unified/JsonApiRequest"); let apiResponse: ApiResponseInterface; // Get language based on environment let language = "en"; if (typeof window === "undefined") { const { getLocale } = await import("next-intl/server"); language = (await getLocale()) ?? "en"; } else { // Client-side: extract locale from URL pathname language = this.getClientLocale(); } switch (params.method) { case HttpMethod.GET: apiResponse = await JsonApiGet({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, language: language, }); break; case HttpMethod.POST: apiResponse = await JsonApiPost({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, overridesJsonApiCreation: params.overridesJsonApiCreation, language: language, responseType: params.responseType, files: params.files, }); break; case HttpMethod.PUT: apiResponse = await JsonApiPut({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, language: language, responseType: params.responseType, files: params.files, }); break; case HttpMethod.PATCH: apiResponse = await JsonApiPatch({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, body: params.input, overridesJsonApiCreation: params.overridesJsonApiCreation, language: language, responseType: params.responseType, files: params.files, }); break; case HttpMethod.DELETE: apiResponse = await JsonApiDelete({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, language: language, responseType: params.responseType, }); break; default: throw new Error("Method not found"); } if (!apiResponse.ok) { if (globalErrorHandler && typeof window !== "undefined" && !params.suppressGlobalError) { globalErrorHandler(apiResponse.response, apiResponse.error); return { data: undefined as any, meta: undefined }; } else { const error = new Error(`${apiResponse.response}:${apiResponse.error}`) as any; error.status = apiResponse.response; error.digest = `HTTP_${apiResponse.response}`; error.body = apiResponse.raw; throw error; } } return { data: apiResponse.data as T, meta: apiResponse.meta, }; } /** * Get raw JSON:API response data without deserialization. */ protected static async getRawData(params: { type: ApiRequestDataTypeInterface; method: HttpMethod; endpoint: string; companyId?: string; baseUrl?: string; suppressGlobalError?: boolean; }): Promise { const { JsonApiGet } = await import("../../unified/JsonApiRequest"); let language = "en"; if (typeof window === "undefined") { const { getLocale } = await import("next-intl/server"); language = (await getLocale()) ?? "en"; } else { // Client-side: extract locale from URL pathname language = this.getClientLocale(); } const apiResponse: ApiResponseInterface = await JsonApiGet({ classKey: params.type, endpoint: params.endpoint, companyId: params.companyId, baseUrl: params.baseUrl, language: language, }); if (!apiResponse.ok) { if (globalErrorHandler && typeof window !== "undefined" && !params.suppressGlobalError) { globalErrorHandler(apiResponse.response, apiResponse.error); return undefined as any; } else { const error = new Error(`${apiResponse.response}:${apiResponse.error}`) as any; error.status = apiResponse.response; error.digest = `HTTP_${apiResponse.response}`; error.body = apiResponse.raw; throw error; } } return apiResponse.raw; } }