chore: update .gitignore and improve coding standards documentation
- Added .cursor/sessions/* to .gitignore to prevent session files from being tracked. - Enhanced coding standards in SKILL.md by adding semicolons to TypeScript examples for consistency. - Improved formatting in continuous learning, detail layout, and other SKILL.md files for better readability. These changes aim to streamline development processes and maintain code quality across the project.
This commit is contained in:
@@ -11,6 +11,7 @@ The platform-agnostic API engine for the monorepo. Provides an isolated HTTP cli
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
%% ─── Styling Definitions (Dark-Mode Friendly Enterprise Palette) ───
|
||||
@@ -46,7 +47,7 @@ graph TD
|
||||
%% ─── Flow & Relationships ───
|
||||
WEB & LAND & DESK ===>|instantiates| FACTORY
|
||||
WEB & LAND & DESK ===>|extends| COMMON
|
||||
|
||||
|
||||
COMMON --->|executes via| FACTORY
|
||||
FACTORY -.->|reports via| FARO
|
||||
FACTORY -.->|throws| API_ERR
|
||||
@@ -61,13 +62,13 @@ graph TD
|
||||
%% ─── Subgraph Backgrounds (Transparent for Native GitHub Support) ───
|
||||
style Apps fill:transparent,stroke:#3b82f6,stroke-width:2px,stroke-dasharray: 5 5
|
||||
style Core fill:transparent,stroke:#94a3b8,stroke-width:2px,stroke-dasharray: 5 5
|
||||
|
||||
|
||||
%% Nested subgraphs also need transparent backgrounds to prevent glaring white boxes in dark mode
|
||||
style HTTP fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||
style OBS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||
style DATA fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||
style ERRORS fill:transparent,stroke:#cbd5e1,stroke-width:1px,stroke-dasharray: 5 5
|
||||
```
|
||||
```
|
||||
|
||||
### Data Flow Lifecycle
|
||||
|
||||
@@ -102,7 +103,7 @@ sequenceDiagram
|
||||
H->>F: onRequestStart() (Log + Span)
|
||||
H->>A: hooks.onRequest() (Inject Token)
|
||||
A->>N: fetch/XHR
|
||||
|
||||
|
||||
alt Success (2xx)
|
||||
N-->>A: return Response
|
||||
A->>F: onRequestEnd() (Close Span)
|
||||
@@ -156,20 +157,20 @@ export const apiClient = createHttpClient(
|
||||
|
||||
### Configuration
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
|---|---|---|---|
|
||||
| `baseURL` | `string` | *required* | Base URL for all requests |
|
||||
| `timeout` | `number` | `15000` | Default request timeout (ms) |
|
||||
| `defaultHeaders` | `Record<string, string>` | `{}` | Headers applied to every request |
|
||||
| `observability` | `IObservabilityAdapter` | `noopAdapter` | Observability adapter (Faro or no-op) |
|
||||
| Property | Type | Default | Description |
|
||||
| ---------------- | ------------------------ | ------------- | ------------------------------------- |
|
||||
| `baseURL` | `string` | _required_ | Base URL for all requests |
|
||||
| `timeout` | `number` | `15000` | Default request timeout (ms) |
|
||||
| `defaultHeaders` | `Record<string, string>` | `{}` | Headers applied to every request |
|
||||
| `observability` | `IObservabilityAdapter` | `noopAdapter` | Observability adapter (Faro or no-op) |
|
||||
|
||||
### Interceptor Hooks
|
||||
|
||||
| Hook | Signature | Purpose |
|
||||
|---|---|---|
|
||||
| `onRequest` | `(config) => config` | Inject auth tokens, tenant headers |
|
||||
| `onResponse` | `(response) => response` | Transform response shapes |
|
||||
| `onResponseError` | `(error) => never` | App-specific error handling (e.g., 401 redirect) |
|
||||
| Hook | Signature | Purpose |
|
||||
| ----------------- | ------------------------ | ------------------------------------------------ |
|
||||
| `onRequest` | `(config) => config` | Inject auth tokens, tenant headers |
|
||||
| `onResponse` | `(response) => response` | Transform response shapes |
|
||||
| `onResponseError` | `(error) => never` | App-specific error handling (e.g., 401 redirect) |
|
||||
|
||||
---
|
||||
|
||||
@@ -179,13 +180,12 @@ export const apiClient = createHttpClient(
|
||||
|
||||
The observability layer operates in two complementary modes:
|
||||
|
||||
| Mode | Activation | What it does |
|
||||
|---|---|---|
|
||||
| **Baseline** (always on) | Automatic | Pushes structured logs to Faro/Loki on every request with `module.key`, `module.action`, HTTP method, and URL |
|
||||
| **Custom Span** (opt-in) | Via `telemetryContext.customSpanName` | Creates an explicit OTel span with custom tags, visible in [Grafana Tempo](https://grafana.com/oss/tempo/) |
|
||||
| Mode | Activation | What it does |
|
||||
| ------------------------ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| **Baseline** (always on) | Automatic | Pushes structured logs to Faro/Loki on every request with `module.key`, `module.action`, HTTP method, and URL |
|
||||
| **Custom Span** (opt-in) | Via `telemetryContext.customSpanName` | Creates an explicit OTel span with custom tags, visible in [Grafana Tempo](https://grafana.com/oss/tempo/) |
|
||||
|
||||
> [!NOTE]
|
||||
> `trace.getActiveSpan()` returns `undefined` inside Axios interceptors due to browser XHR/Fetch lifecycle race conditions with Faro's `TracingInstrumentation`. The adapter does **not** attempt to enrich auto-instrumented spans. HTTP span capture is handled entirely by `TracingInstrumentation` auto-instrumentation.
|
||||
> [!NOTE] > `trace.getActiveSpan()` returns `undefined` inside Axios interceptors due to browser XHR/Fetch lifecycle race conditions with Faro's `TracingInstrumentation`. The adapter does **not** attempt to enrich auto-instrumented spans. HTTP span capture is handled entirely by `TracingInstrumentation` auto-instrumentation.
|
||||
|
||||
### Initialization
|
||||
|
||||
@@ -206,34 +206,34 @@ initTelemetry({
|
||||
|
||||
### `TelemetryConfig`
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `appName` | `string` | ✅ | Application name for Faro + OTel resource attributes |
|
||||
| `appVersion` | `string` | ✅ | SemVer version |
|
||||
| `telemetryUrl` | `string` | ✅ | Grafana Faro collector URL |
|
||||
| `environment` | `string` | ✅ | Deployment environment (`production`, `staging`, `development`) |
|
||||
| `otlpTraceUrl` | `string` | — | Separate OTLP trace endpoint for direct Tempo ingestion |
|
||||
| `propagateTraceHeaderCorsUrls` | `Array<string \| RegExp>` | — | CORS patterns for W3C trace context propagation (default: `[/.*/]`) |
|
||||
| Property | Type | Required | Description |
|
||||
| ------------------------------ | ------------------------- | -------- | ------------------------------------------------------------------- |
|
||||
| `appName` | `string` | ✅ | Application name for Faro + OTel resource attributes |
|
||||
| `appVersion` | `string` | ✅ | SemVer version |
|
||||
| `telemetryUrl` | `string` | ✅ | Grafana Faro collector URL |
|
||||
| `environment` | `string` | ✅ | Deployment environment (`production`, `staging`, `development`) |
|
||||
| `otlpTraceUrl` | `string` | — | Separate OTLP trace endpoint for direct Tempo ingestion |
|
||||
| `propagateTraceHeaderCorsUrls` | `Array<string \| RegExp>` | — | CORS patterns for W3C trace context propagation (default: `[/.*/]`) |
|
||||
|
||||
### Audit Headers
|
||||
|
||||
Every request dispatched through `BaseRemoteDataServices` automatically attaches two business audit headers:
|
||||
|
||||
| Header | Source | Purpose |
|
||||
|---|---|---|
|
||||
| `ex-module-key` | `DataServicesConfig.moduleKey` | Identifies the business module (e.g., `BOOKING`) |
|
||||
| `ex-module-action` | `RequestDescriptor.action` | Identifies the operation (e.g., `READ`, `CREATE`) |
|
||||
| Header | Source | Purpose |
|
||||
| ------------------ | ------------------------------ | ------------------------------------------------- |
|
||||
| `ex-module-key` | `DataServicesConfig.moduleKey` | Identifies the business module (e.g., `BOOKING`) |
|
||||
| `ex-module-action` | `RequestDescriptor.action` | Identifies the operation (e.g., `READ`, `CREATE`) |
|
||||
|
||||
These headers are extracted by the `faroAdapter` and included in all Faro `pushLog`, `pushError`, and `pushEvent` calls as top-level context — making them directly queryable in **LogQL ([Loki](https://grafana.com/oss/loki/))**.
|
||||
|
||||
### Span Safety Guarantees
|
||||
|
||||
| Guarantee | Mechanism |
|
||||
|---|---|
|
||||
| **No span leaks** | `safeEndSpan()` always closes the span and detaches the reference from config |
|
||||
| **No double-close on retry** | Span reference is deleted from config after `span.end()` |
|
||||
| **No error swallowing** | All adapter calls are wrapped in try-catch in `create-http-client.ts` |
|
||||
| **No crash on timeout** | `null`/`undefined` config guards on all `error.config` access |
|
||||
| Guarantee | Mechanism |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------- |
|
||||
| **No span leaks** | `safeEndSpan()` always closes the span and detaches the reference from config |
|
||||
| **No double-close on retry** | Span reference is deleted from config after `span.end()` |
|
||||
| **No error swallowing** | All adapter calls are wrapped in try-catch in `create-http-client.ts` |
|
||||
| **No crash on timeout** | `null`/`undefined` config guards on all `error.config` access |
|
||||
|
||||
---
|
||||
|
||||
@@ -254,32 +254,29 @@ interface BookingEntity extends BaseEntity {
|
||||
status: 'pending' | 'confirmed' | 'cancelled';
|
||||
}
|
||||
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
apiClient,
|
||||
{
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
},
|
||||
);
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(apiClient, {
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
});
|
||||
```
|
||||
|
||||
### Available Operations
|
||||
|
||||
| Method | HTTP | URL Template | Description |
|
||||
|---|---|---|---|
|
||||
| `getMany(config?)` | GET | `/bookings` | Fetch paginated list |
|
||||
| `getOne(id, config?)` | GET | `/bookings/:id` | Fetch single entity |
|
||||
| `create(data, config?)` | POST | `/bookings` | Create new entity |
|
||||
| `edit(id, data, config?)` | PUT | `/bookings/:id` | Update entity |
|
||||
| `delete(id, config?)` | DELETE | `/bookings/:id` | Delete entity |
|
||||
| `batchDelete(ids, config?)` | DELETE | `/bookings/batch` | Delete multiple |
|
||||
| `activate(id)` | PATCH | `/bookings/:id/activate` | Activate entity |
|
||||
| `deactivate(id)` | PATCH | `/bookings/:id/deactivate` | Deactivate entity |
|
||||
| `confirmProcessData(id)` | PATCH | `/bookings/:id/confirm-process-data` | Confirm data processing |
|
||||
| `confirmProcessTransaction(id)` | PATCH | `/bookings/:id/confirm-process-transaction` | Confirm transaction |
|
||||
| `cancelProcessTransaction(id)` | PATCH | `/bookings/:id/cancel-process-transaction` | Cancel transaction |
|
||||
| `rollbackProcessTransaction(id)` | PATCH | `/bookings/:id/rollback-process-transaction` | Rollback transaction |
|
||||
| `holdProcessTransaction(id)` | PATCH | `/bookings/:id/hold-process-transaction` | Hold transaction |
|
||||
| Method | HTTP | URL Template | Description |
|
||||
| -------------------------------- | ------ | -------------------------------------------- | ----------------------- |
|
||||
| `getMany(config?)` | GET | `/bookings` | Fetch paginated list |
|
||||
| `getOne(id, config?)` | GET | `/bookings/:id` | Fetch single entity |
|
||||
| `create(data, config?)` | POST | `/bookings` | Create new entity |
|
||||
| `edit(id, data, config?)` | PUT | `/bookings/:id` | Update entity |
|
||||
| `delete(id, config?)` | DELETE | `/bookings/:id` | Delete entity |
|
||||
| `batchDelete(ids, config?)` | DELETE | `/bookings/batch` | Delete multiple |
|
||||
| `activate(id)` | PATCH | `/bookings/:id/activate` | Activate entity |
|
||||
| `deactivate(id)` | PATCH | `/bookings/:id/deactivate` | Deactivate entity |
|
||||
| `confirmProcessData(id)` | PATCH | `/bookings/:id/confirm-process-data` | Confirm data processing |
|
||||
| `confirmProcessTransaction(id)` | PATCH | `/bookings/:id/confirm-process-transaction` | Confirm transaction |
|
||||
| `cancelProcessTransaction(id)` | PATCH | `/bookings/:id/cancel-process-transaction` | Cancel transaction |
|
||||
| `rollbackProcessTransaction(id)` | PATCH | `/bookings/:id/rollback-process-transaction` | Rollback transaction |
|
||||
| `holdProcessTransaction(id)` | PATCH | `/bookings/:id/hold-process-transaction` | Hold transaction |
|
||||
|
||||
All batch variants (`batchActivate`, `batchDeactivate`, etc.) are also available.
|
||||
|
||||
@@ -308,8 +305,11 @@ import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||
initTelemetry({
|
||||
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-web',
|
||||
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||
telemetryUrl: import.meta.env.VITE_FARO_URL || '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)',
|
||||
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || '[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)',
|
||||
telemetryUrl:
|
||||
import.meta.env.VITE_FARO_URL || '[https://telemetry.eigen.co.id/collect](https://telemetry.eigen.co.id/collect)',
|
||||
otlpTraceUrl:
|
||||
import.meta.env.VITE_OTLP_TRACE_URL ||
|
||||
'[https://telemetry.eigen.co.id/v1/traces](https://telemetry.eigen.co.id/v1/traces)',
|
||||
environment: import.meta.env.VITE_ENV || 'development',
|
||||
});
|
||||
|
||||
@@ -345,10 +345,10 @@ export interface BookingEntity extends BaseEntity {
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||
apiClient,
|
||||
{ apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||
);
|
||||
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(apiClient, {
|
||||
apiUrl: '/bookings',
|
||||
moduleKey: 'BOOKING',
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Consume in a React Component
|
||||
@@ -425,11 +425,11 @@ await bookingServices.getMany({
|
||||
|
||||
### What Happens at Each Stage
|
||||
|
||||
| Stage | Baseline (no telemetryContext) | With `customSpanName` |
|
||||
|---|---|---|
|
||||
| **Request Start** | Faro `pushLog` (DEBUG) with `module.key`, `module.action`, URL | + Creates OTel span with `http.method`, `http.url`, `custom.*` tags |
|
||||
| **Request Success** | — | Closes span (OK). If `pushEventOnSuccess`, pushes Faro event |
|
||||
| **Request Error** | Faro `pushError` + `pushLog` (ERROR) | + Closes span (ERROR), records exception |
|
||||
| Stage | Baseline (no telemetryContext) | With `customSpanName` |
|
||||
| ------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| **Request Start** | Faro `pushLog` (DEBUG) with `module.key`, `module.action`, URL | + Creates OTel span with `http.method`, `http.url`, `custom.*` tags |
|
||||
| **Request Success** | — | Closes span (OK). If `pushEventOnSuccess`, pushes Faro event |
|
||||
| **Request Error** | Faro `pushError` + `pushLog` (ERROR) | + Closes span (ERROR), records exception |
|
||||
|
||||
---
|
||||
|
||||
@@ -444,10 +444,10 @@ try {
|
||||
await bookingServices.getOne('42');
|
||||
} catch (err) {
|
||||
if (err instanceof ApiError) {
|
||||
err.code; // ApiErrorCode.NOT_FOUND
|
||||
err.status; // 404
|
||||
err.code; // ApiErrorCode.NOT_FOUND
|
||||
err.status; // 404
|
||||
err.message; // "Booking not found"
|
||||
err.data; // Raw server response body
|
||||
err.data; // Raw server response body
|
||||
err.toJSON(); // Serializable for logging
|
||||
}
|
||||
}
|
||||
@@ -455,25 +455,25 @@ try {
|
||||
|
||||
### Error Codes
|
||||
|
||||
| Code | HTTP Status | Description |
|
||||
|---|---|---|
|
||||
| `BAD_REQUEST` | 400 | Invalid request parameters |
|
||||
| `UNAUTHORIZED` | 401 | Missing or expired token |
|
||||
| `FORBIDDEN` | 403 | Insufficient permissions |
|
||||
| `NOT_FOUND` | 404 | Resource not found |
|
||||
| `TIMEOUT` | — | Request timed out (`ECONNABORTED`) |
|
||||
| `CANCELLED` | — | Request was cancelled (`ERR_CANCELED`) |
|
||||
| `NETWORK_ERROR` | — | No response received |
|
||||
| `SERVER_ERROR` | 500+ | Internal server error |
|
||||
| Code | HTTP Status | Description |
|
||||
| --------------- | ----------- | -------------------------------------- |
|
||||
| `BAD_REQUEST` | 400 | Invalid request parameters |
|
||||
| `UNAUTHORIZED` | 401 | Missing or expired token |
|
||||
| `FORBIDDEN` | 403 | Insufficient permissions |
|
||||
| `NOT_FOUND` | 404 | Resource not found |
|
||||
| `TIMEOUT` | — | Request timed out (`ECONNABORTED`) |
|
||||
| `CANCELLED` | — | Request was cancelled (`ERR_CANCELED`) |
|
||||
| `NETWORK_ERROR` | — | No response received |
|
||||
| `SERVER_ERROR` | 500+ | Internal server error |
|
||||
|
||||
---
|
||||
|
||||
## Package Exports
|
||||
|
||||
| Import Path | Contents |
|
||||
|---|---|
|
||||
| `@repo/core-api/http-client` | `createHttpClient`, `ApiResponse`, `TelemetryContext`, Axios type re-exports |
|
||||
| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants |
|
||||
| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` |
|
||||
| Import Path | Contents |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
|
||||
| `@repo/core-api/http-client` | `createHttpClient`, `ApiResponse`, `TelemetryContext`, Axios type re-exports |
|
||||
| `@repo/core-api/observability` | `faroAdapter`, `noopObservabilityAdapter`, `IObservabilityAdapter`, `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/observability/setup` | `initTelemetry`, `getFaro`, `TelemetryConfig` |
|
||||
| `@repo/core-api/data-services` | `BaseRemoteDataServices`, `CommonRemoteDataServices`, types, constants |
|
||||
| `@repo/core-api/errors` | `ApiError`, `ApiErrorCode` |
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
In enterprise applications, the shape of data returned by the API (DTOs) often differs from the shape used in the frontend (Domain Entities). Common differences include:
|
||||
|
||||
| API (DTO) | Frontend (Entity) |
|
||||
| ------------------------------ | ---------------------------- |
|
||||
| `snake_case` field names | `camelCase` field names |
|
||||
| Deeply nested structures | Flattened/normalized shapes |
|
||||
| Raw ISO date strings | Parsed `Date` objects |
|
||||
| No computed fields | Derived/computed properties |
|
||||
| Backend-specific enums | Frontend-friendly enums |
|
||||
| API (DTO) | Frontend (Entity) |
|
||||
| ------------------------ | --------------------------- |
|
||||
| `snake_case` field names | `camelCase` field names |
|
||||
| Deeply nested structures | Flattened/normalized shapes |
|
||||
| Raw ISO date strings | Parsed `Date` objects |
|
||||
| No computed fields | Derived/computed properties |
|
||||
| Backend-specific enums | Frontend-friendly enums |
|
||||
|
||||
Without transformers, this mapping logic leaks into components, hooks, and services — violating the **Single Responsibility Principle** and making the codebase harder to test and maintain.
|
||||
|
||||
@@ -54,6 +54,7 @@ graph LR
|
||||
```
|
||||
|
||||
**Data flows:**
|
||||
|
||||
- **API → Frontend:** Response DTO → `transformToEntity()` → Domain Entity
|
||||
- **Frontend → API:** Domain Entity → `transformToDTO()` → Request DTO
|
||||
|
||||
@@ -153,14 +154,14 @@ interface IDataTransformer<TEntity, TDTO> {
|
||||
|
||||
Abstract class implementing `IDataTransformer` with sensible defaults.
|
||||
|
||||
| Method | Default Behavior | Override When |
|
||||
| ------------------------- | ---------------------------------------- | ------------------------------------------ |
|
||||
| `transformToEntity` | Identity cast (passthrough) | Always — this is the core mapping |
|
||||
| `transformToDTO` | Identity cast (passthrough) | Always — this is the core mapping |
|
||||
| `transformGetOneResponse` | Delegates to `transformToEntity` | `getOne` needs computed/derived fields |
|
||||
| `transformGetManyResponse`| Maps each item via `transformToEntity` | List responses need bulk transformations |
|
||||
| `transformCreatePayload` | Delegates to `transformToDTO` | Create payloads need special handling (e.g., strip IDs) |
|
||||
| `transformEditPayload` | Delegates to `transformToDTO` | Edit payloads differ from create |
|
||||
| Method | Default Behavior | Override When |
|
||||
| -------------------------- | -------------------------------------- | ------------------------------------------------------- |
|
||||
| `transformToEntity` | Identity cast (passthrough) | Always — this is the core mapping |
|
||||
| `transformToDTO` | Identity cast (passthrough) | Always — this is the core mapping |
|
||||
| `transformGetOneResponse` | Delegates to `transformToEntity` | `getOne` needs computed/derived fields |
|
||||
| `transformGetManyResponse` | Maps each item via `transformToEntity` | List responses need bulk transformations |
|
||||
| `transformCreatePayload` | Delegates to `transformToDTO` | Create payloads need special handling (e.g., strip IDs) |
|
||||
| `transformEditPayload` | Delegates to `transformToDTO` | Edit payloads differ from create |
|
||||
|
||||
---
|
||||
|
||||
@@ -168,14 +169,14 @@ Abstract class implementing `IDataTransformer` with sensible defaults.
|
||||
|
||||
When a transformer is injected via `DataServicesConfig.transformer`, the base service methods automatically apply transformations:
|
||||
|
||||
| Service Method | Transformer Hook Used | Direction |
|
||||
| -------------- | -------------------------------- | --------------- |
|
||||
| `getOne()` | `transformGetOneResponse()` | Response → Entity |
|
||||
| `getMany()` | `transformGetManyResponse()` | Response → Entity |
|
||||
| `create()` | `transformCreatePayload()` | Entity → DTO |
|
||||
| `edit()` | `transformEditPayload()` | Entity → DTO |
|
||||
| `delete()` | None (no data transformation) | — |
|
||||
| `customRequest()` | None (manual transformation) | — |
|
||||
| Service Method | Transformer Hook Used | Direction |
|
||||
| ----------------- | ----------------------------- | ----------------- |
|
||||
| `getOne()` | `transformGetOneResponse()` | Response → Entity |
|
||||
| `getMany()` | `transformGetManyResponse()` | Response → Entity |
|
||||
| `create()` | `transformCreatePayload()` | Entity → DTO |
|
||||
| `edit()` | `transformEditPayload()` | Entity → DTO |
|
||||
| `delete()` | None (no data transformation) | — |
|
||||
| `customRequest()` | None (manual transformation) | — |
|
||||
|
||||
> **Important:** If no transformer is injected, all methods behave exactly as before — data passes through unchanged. This ensures 100% backward compatibility.
|
||||
|
||||
@@ -277,8 +278,12 @@ Adding transformers to existing services requires **zero breaking changes**:
|
||||
|
||||
```typescript
|
||||
class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
transformToEntity(dto: MyDTO): MyEntity { /* ... */ }
|
||||
transformToDTO(entity: MyEntity): MyDTO { /* ... */ }
|
||||
transformToEntity(dto: MyDTO): MyEntity {
|
||||
/* ... */
|
||||
}
|
||||
transformToDTO(entity: MyEntity): MyDTO {
|
||||
/* ... */
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -296,8 +301,12 @@ class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
|
||||
```typescript
|
||||
class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
transformToEntity(dto: MyDTO): MyEntity { /* ... */ }
|
||||
transformToDTO(entity: MyEntity): MyDTO { /* ... */ }
|
||||
transformToEntity(dto: MyDTO): MyEntity {
|
||||
/* ... */
|
||||
}
|
||||
transformToDTO(entity: MyEntity): MyDTO {
|
||||
/* ... */
|
||||
}
|
||||
|
||||
// Only override if getOne needs special handling
|
||||
override transformGetOneResponse(dto: MyDTO): MyEntity {
|
||||
@@ -315,9 +324,9 @@ class MyTransformer extends BaseDataTransformer<MyEntity, MyDTO> {
|
||||
|
||||
A full working example is available in the showcase booking feature:
|
||||
|
||||
| File | Description |
|
||||
| ---- | ----------- |
|
||||
| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping |
|
||||
| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` |
|
||||
| File | Description |
|
||||
| ------------------------------------------------------------------ | ------------------------------------------------------ |
|
||||
| `apps/showcase/.../booking/data/booking.transformer.ts` | Basic transformer with snake_case ↔ camelCase mapping |
|
||||
| `apps/showcase/.../booking/data/booking.data-services.ts` | Data service with injected transformer |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.transformer.ts` | Extended transformer with custom chart method |
|
||||
| `apps/showcase/.../booking/data/advanced-booking.data-services.ts` | Extended service with custom `getAvailabilityChart()` |
|
||||
|
||||
Reference in New Issue
Block a user