361 lines
13 KiB
TypeScript
361 lines
13 KiB
TypeScript
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|
import type {
|
|
BaseEntity,
|
|
ApiURLMap,
|
|
RequestMethodMap,
|
|
RequestDescriptor,
|
|
ExecuteOptions,
|
|
DataServicesConfig,
|
|
EntityId,
|
|
} from './types';
|
|
import type { IDataTransformer } from './base-data.transformer';
|
|
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<ApiResponse<T>>` (no callback-based onSuccess/onFailed)
|
|
* - All operations are fully typed end-to-end
|
|
* - Includes `customRequest<T>()` as an escape hatch for non-standard endpoints
|
|
*
|
|
* @typeParam E - The domain entity type (must extend BaseEntity)
|
|
* @typeParam TDTO - The API DTO shape (defaults to E for backward compatibility)
|
|
*
|
|
* @example
|
|
* ```ts
|
|
* // Without transformer (backward compatible)
|
|
* class BookingDataServices extends BaseRemoteDataServices<BookingEntity> {}
|
|
*
|
|
* const services = new BookingDataServices(apiClient, {
|
|
* apiUrl: '/bookings',
|
|
* moduleKey: 'BOOKING',
|
|
* });
|
|
*
|
|
* // With transformer (DTO ↔ Entity mapping)
|
|
* const services = new BookingDataServices(apiClient, {
|
|
* apiUrl: '/bookings',
|
|
* moduleKey: 'BOOKING',
|
|
* transformer: new BookingTransformer(),
|
|
* });
|
|
*
|
|
* const { data, status } = await services.getOne<BookingEntity>('42');
|
|
* ```
|
|
*/
|
|
export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity, TDTO = E> {
|
|
/** 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;
|
|
|
|
/**
|
|
* Optional data transformer for DTO ↔ Entity mapping.
|
|
*
|
|
* When present, CRUD methods automatically transform responses
|
|
* and payloads. When absent, data passes through unchanged.
|
|
*/
|
|
protected readonly transformer: IDataTransformer<E, TDTO> | undefined;
|
|
|
|
constructor(httpClient: AxiosInstance, config: DataServicesConfig<E, TDTO>) {
|
|
this.httpClient = httpClient;
|
|
this.moduleKey = config.moduleKey;
|
|
this.transformer = config.transformer;
|
|
|
|
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<T = unknown>(
|
|
descriptor: RequestDescriptor,
|
|
options?: ExecuteOptions,
|
|
): Promise<ApiResponse<T>> {
|
|
const { urlKey, methodKey, action } = descriptor;
|
|
|
|
const response = await this.httpClient.request<T>({
|
|
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<TaxResult>({
|
|
* url: '/bookings/42/calculate-tax',
|
|
* method: 'POST',
|
|
* data: { items: [...] },
|
|
* });
|
|
* ```
|
|
*/
|
|
async customRequest<T = unknown>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const response = await this.httpClient.request<T>({
|
|
...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.
|
|
*
|
|
* When a transformer is injected, the raw API response is passed
|
|
* through `transformGetManyResponse()` before being returned.
|
|
*/
|
|
async getMany<T = E[]>(config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const finalConfig = config?.params && this.transformer?.transformPayloadFilter
|
|
? { ...config, params: this.transformer.transformPayloadFilter(config.params) }
|
|
: config;
|
|
|
|
const result = await this.execute<T>(DESCRIPTORS.getMany, { config: finalConfig });
|
|
|
|
if (this.transformer && Array.isArray(result.data)) {
|
|
return {
|
|
...result,
|
|
data: this.transformer.transformGetManyResponse
|
|
? this.transformer.transformGetManyResponse(result.data as unknown as TDTO[])
|
|
: result.data.map((item: unknown) => this.transformer!.transformToEntity(item as TDTO)),
|
|
} as unknown as ApiResponse<T>;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Fetch a single entity by ID.
|
|
*
|
|
* When a transformer is injected, the raw API response is passed
|
|
* through `transformGetOneResponse()` before being returned.
|
|
*/
|
|
async getOne<T = E>(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const result = await this.execute<T>(DESCRIPTORS.getOne, {
|
|
variableURL: { id },
|
|
config,
|
|
});
|
|
|
|
if (this.transformer && result.data != null) {
|
|
return {
|
|
...result,
|
|
data: this.transformer.transformGetOneResponse
|
|
? this.transformer.transformGetOneResponse(result.data as unknown as TDTO)
|
|
: this.transformer.transformToEntity(result.data as unknown as TDTO),
|
|
} as unknown as ApiResponse<T>;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a new entity.
|
|
*
|
|
* When a transformer is injected, the entity payload is passed
|
|
* through `transformCreatePayload()` before being sent to the API.
|
|
*/
|
|
create<T = E>(data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const transformedData = this.transformer?.transformCreatePayload
|
|
? this.transformer.transformCreatePayload(data)
|
|
: this.transformer
|
|
? this.transformer.transformToDTO(data as E)
|
|
: data;
|
|
|
|
return this.execute<T>(DESCRIPTORS.create, {
|
|
config: { ...config, data: transformedData },
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update an existing entity by ID.
|
|
*
|
|
* When a transformer is injected, the entity payload is passed
|
|
* through `transformEditPayload()` before being sent to the API.
|
|
*/
|
|
edit<T = E>(id: string, data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
|
const transformedData = this.transformer?.transformEditPayload
|
|
? this.transformer.transformEditPayload(data)
|
|
: this.transformer
|
|
? this.transformer.transformToDTO(data as E)
|
|
: data;
|
|
|
|
return this.execute<T>(DESCRIPTORS.edit, {
|
|
variableURL: { id },
|
|
config: { ...config, data: transformedData },
|
|
});
|
|
}
|
|
|
|
/** Delete a single entity by ID. Optionally sends form data as `meta` in the request body. */
|
|
delete(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.delete, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Delete multiple entities by IDs. */
|
|
batchDelete(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchDelete, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
// ─── Activation Lifecycle ─────────────────────────────────────
|
|
|
|
/** Activate a single entity. Optionally sends form data as `meta` in the request body. */
|
|
activate(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.activate, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Activate multiple entities. */
|
|
batchActivate(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchActivate, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
/** Deactivate a single entity. Optionally sends form data as `meta` in the request body. */
|
|
deactivate(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.deactivate, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Deactivate multiple entities. */
|
|
batchDeactivate(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchDeactivate, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
// ─── Data Processing Lifecycle ────────────────────────────────
|
|
|
|
/** Confirm processing of a single data record. Optionally sends form data as `meta` in the request body. */
|
|
confirmData(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.confirmData, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Confirm processing of multiple data records. */
|
|
batchConfirmData(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchConfirmData, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
/** Cancel processing of a single data record. Optionally sends form data as `meta` in the request body. */
|
|
cancelData(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.cancelData, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Cancel processing of multiple data records. */
|
|
batchCancelData(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchCancelData, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
// ─── Transaction Lifecycle ────────────────────────────────────
|
|
|
|
/** Rollback a transaction. Optionally sends form data as `meta` in the request body. */
|
|
rollbackData(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.rollbackData, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Rollback multiple transactions. */
|
|
batchRollbackData(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchRollbackData, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
|
|
/** Hold a transaction. Optionally sends form data as `meta` in the request body. */
|
|
holdData(id: EntityId, meta?: Record<string, unknown>, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.holdData, {
|
|
variableURL: { id },
|
|
config: { ...config, ...(meta ? { data: { meta } } : {}) },
|
|
});
|
|
}
|
|
|
|
/** Hold multiple transactions. */
|
|
batchHoldData(ids: EntityId[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
|
return this.execute<void>(DESCRIPTORS.batchHoldData, {
|
|
config: { ...config, data: { ids } },
|
|
});
|
|
}
|
|
}
|