Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -0,0 +1,314 @@
|
||||
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<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)
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* class BookingDataServices extends BaseRemoteDataServices<BookingEntity> {}
|
||||
*
|
||||
* const services = new BookingDataServices(apiClient, {
|
||||
* apiUrl: '/bookings',
|
||||
* moduleKey: 'BOOKING',
|
||||
* });
|
||||
*
|
||||
* const { data, status } = await services.getOne<BookingEntity>('42');
|
||||
* ```
|
||||
*/
|
||||
export abstract class BaseRemoteDataServices<E extends BaseEntity = BaseEntity> {
|
||||
/** 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<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. */
|
||||
getMany<T = E[]>(config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.getMany, { config });
|
||||
}
|
||||
|
||||
/** Fetch a single entity by ID. */
|
||||
getOne<T = E>(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.getOne, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Create a new entity. */
|
||||
create<T = E>(data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.create, {
|
||||
config: { ...config, data },
|
||||
});
|
||||
}
|
||||
|
||||
/** Update an existing entity by ID. */
|
||||
edit<T = E>(id: string, data: Partial<E>, config?: AxiosRequestConfig): Promise<ApiResponse<T>> {
|
||||
return this.execute<T>(DESCRIPTORS.edit, {
|
||||
variableURL: { id },
|
||||
config: { ...config, data },
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete a single entity by ID. */
|
||||
delete(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.delete, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Delete multiple entities by IDs. */
|
||||
batchDelete(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchDelete, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Activation Lifecycle ─────────────────────────────────────
|
||||
|
||||
/** Activate a single entity. */
|
||||
activate(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.activate, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Activate multiple entities. */
|
||||
batchActivate(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchActivate, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Deactivate a single entity. */
|
||||
deactivate(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.deactivate, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Deactivate multiple entities. */
|
||||
batchDeactivate(ids: string[], 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. */
|
||||
confirmProcessData(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.confirmProcessData, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Confirm processing of multiple data records. */
|
||||
batchConfirmProcessData(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchConfirmProcessData, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel processing of a single data record. */
|
||||
cancelProcessData(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.cancelProcessData, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel processing of multiple data records. */
|
||||
batchCancelProcessData(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchCancelProcessData, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Transaction Lifecycle ────────────────────────────────────
|
||||
|
||||
/** Confirm a transaction. */
|
||||
confirmProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.confirmProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Confirm multiple transactions. */
|
||||
batchConfirmProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchConfirmProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel a transaction. */
|
||||
cancelProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.cancelProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Cancel multiple transactions. */
|
||||
batchCancelProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchCancelProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Rollback a transaction. */
|
||||
rollbackProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.rollbackProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Rollback multiple transactions. */
|
||||
batchRollbackProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchRollbackProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
|
||||
/** Hold a transaction. */
|
||||
holdProcessTransaction(id: string, config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.holdProcessTransaction, {
|
||||
variableURL: { id },
|
||||
config,
|
||||
});
|
||||
}
|
||||
|
||||
/** Hold multiple transactions. */
|
||||
batchHoldProcessTransaction(ids: string[], config?: AxiosRequestConfig): Promise<ApiResponse<void>> {
|
||||
return this.execute<void>(DESCRIPTORS.batchHoldProcessTransaction, {
|
||||
config: { ...config, data: { ids } },
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user