import type { AxiosInstance, AxiosRequestConfig } from 'axios'; import type { BaseEntity, ApiURLMap, RequestMethodMap, RequestDescriptor, ExecuteOptions, DataServicesConfig, } from './types'; import type { ApiResponse } from '../http-client/types'; import { interpolateUrl } from './url-builder'; import { DEFAULT_METHODS, DESCRIPTORS, makeDefaultURLs } from './constants'; /** * Abstract base class for remote data services. * * Provides a generic `execute()` method that eliminates the 22 * near-identical methods from the legacy `BaseRemoteDataServices`. * Each operation is reduced to a one-liner calling `execute()` * with the appropriate descriptor. * * **Key architectural differences from legacy:** * - Receives an **injected** AxiosInstance (no global axios import) * - Returns `Promise>` (no callback-based onSuccess/onFailed) * - All operations are fully typed end-to-end * - Includes `customRequest()` as an escape hatch for non-standard endpoints * * @typeParam E - The domain entity type (must extend BaseEntity) * * @example * ```ts * class BookingDataServices extends BaseRemoteDataServices {} * * const services = new BookingDataServices(apiClient, { * apiUrl: '/bookings', * moduleKey: 'BOOKING', * }); * * const { data, status } = await services.getOne('42'); * ``` */ export abstract class BaseRemoteDataServices { /** The injected, isolated HTTP client instance. */ protected readonly httpClient: AxiosInstance; /** Resolved URL map for all operations. */ protected readonly urls: ApiURLMap; /** Resolved HTTP method map for all operations. */ protected readonly methods: RequestMethodMap; /** Module key for the 'ex-module-key' audit header. */ protected readonly moduleKey: string | undefined; constructor(httpClient: AxiosInstance, config: DataServicesConfig) { this.httpClient = httpClient; this.moduleKey = config.moduleKey; this.urls = { ...makeDefaultURLs(config.apiUrl ?? ''), ...(config.urls ?? {}), }; this.methods = { ...DEFAULT_METHODS, ...(config.methods ?? {}), }; } // ─── Generic Executor ─────────────────────────────────────────── /** * The single generic request executor. * * All standard operations delegate to this method with a * pre-defined descriptor. This is the engine that replaces * 22 near-identical legacy methods. * * @typeParam T - Expected response data type * @param descriptor - Defines which URL, method, and action to use * @param options - Dynamic URL params and additional Axios config * @returns Typed API response with data and status */ protected async execute( descriptor: RequestDescriptor, options?: ExecuteOptions, ): Promise> { const { urlKey, methodKey, action } = descriptor; const response = await this.httpClient.request({ url: interpolateUrl(this.urls[urlKey], options?.variableURL), method: this.methods[methodKey], ...(options?.config ?? {}), headers: { ...(this.moduleKey ? { 'ex-module-key': this.moduleKey } : {}), 'ex-module-action': action, ...(options?.config?.headers ?? {}), }, telemetryContext: options?.telemetryContext ?? options?.config?.telemetryContext // FIXME, }); return { data: response.data, status: response.status }; } // ─── Escape Hatch ─────────────────────────────────────────────── /** * Execute a fully custom request that doesn't fit standard CRUD. * * Use this for non-standard endpoints like `/calculate-tax`, * custom aggregations, or third-party integrations. * * The request still flows through the injected httpClient, so * all interceptors (auth, observability, error handling) are * preserved automatically. * * @typeParam T - Expected response data type * @param config - Complete Axios request configuration * @returns Typed API response with data and status * * @example * ```ts * const tax = await services.customRequest({ * url: '/bookings/42/calculate-tax', * method: 'POST', * data: { items: [...] }, * }); * ``` */ async customRequest( config: AxiosRequestConfig, ): Promise> { const response = await this.httpClient.request({ ...config, headers: { ...(this.moduleKey ? { 'ex-module-key': this.moduleKey } : {}), ...(config.headers ?? {}), }, }); return { data: response.data, status: response.status }; } // ─── CRUD Operations ─────────────────────────────────────────── /** Fetch a paginated list of entities. */ getMany(config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.getMany, { config }); } /** Fetch a single entity by ID. */ getOne(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.getOne, { variableURL: { id }, config, }); } /** Create a new entity. */ create(data: Partial, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.create, { config: { ...config, data }, }); } /** Update an existing entity by ID. */ edit(id: string, data: Partial, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.edit, { variableURL: { id }, config: { ...config, data }, }); } /** Delete a single entity by ID. */ delete(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.delete, { variableURL: { id }, config, }); } /** Delete multiple entities by IDs. */ batchDelete(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchDelete, { config: { ...config, data: { ids } }, }); } // ─── Activation Lifecycle ───────────────────────────────────── /** Activate a single entity. */ activate(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.activate, { variableURL: { id }, config, }); } /** Activate multiple entities. */ batchActivate(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchActivate, { config: { ...config, data: { ids } }, }); } /** Deactivate a single entity. */ deactivate(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.deactivate, { variableURL: { id }, config, }); } /** Deactivate multiple entities. */ batchDeactivate(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchDeactivate, { config: { ...config, data: { ids } }, }); } // ─── Data Processing Lifecycle ──────────────────────────────── /** Confirm processing of a single data record. */ confirmProcessData(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.confirmProcessData, { variableURL: { id }, config, }); } /** Confirm processing of multiple data records. */ batchConfirmProcessData(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchConfirmProcessData, { config: { ...config, data: { ids } }, }); } /** Cancel processing of a single data record. */ cancelProcessData(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.cancelProcessData, { variableURL: { id }, config, }); } /** Cancel processing of multiple data records. */ batchCancelProcessData(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchCancelProcessData, { config: { ...config, data: { ids } }, }); } // ─── Transaction Lifecycle ──────────────────────────────────── /** Confirm a transaction. */ confirmProcessTransaction(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.confirmProcessTransaction, { variableURL: { id }, config, }); } /** Confirm multiple transactions. */ batchConfirmProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchConfirmProcessTransaction, { config: { ...config, data: { ids } }, }); } /** Cancel a transaction. */ cancelProcessTransaction(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.cancelProcessTransaction, { variableURL: { id }, config, }); } /** Cancel multiple transactions. */ batchCancelProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchCancelProcessTransaction, { config: { ...config, data: { ids } }, }); } /** Rollback a transaction. */ rollbackProcessTransaction(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.rollbackProcessTransaction, { variableURL: { id }, config, }); } /** Rollback multiple transactions. */ batchRollbackProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchRollbackProcessTransaction, { config: { ...config, data: { ids } }, }); } /** Hold a transaction. */ holdProcessTransaction(id: string, config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.holdProcessTransaction, { variableURL: { id }, config, }); } /** Hold multiple transactions. */ batchHoldProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise> { return this.execute(DESCRIPTORS.batchHoldProcessTransaction, { config: { ...config, data: { ids } }, }); } }