Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -41,3 +41,6 @@ storybook-static
|
|||||||
out/
|
out/
|
||||||
release/
|
release/
|
||||||
web-dist/
|
web-dist/
|
||||||
|
|
||||||
|
# Legacy Code (if applicable)
|
||||||
|
legacy/
|
||||||
@@ -33,6 +33,7 @@ The monorepo is organized into **Apps** (deployable applications) and **Packages
|
|||||||
│ └── docs-dev/ # Component Documentation & Playground (Storybook)
|
│ └── docs-dev/ # Component Documentation & Playground (Storybook)
|
||||||
│
|
│
|
||||||
├── packages/
|
├── packages/
|
||||||
|
│ ├── core-api/ # Shared HTTP Client, Observability & Data Services Engine
|
||||||
│ ├── ui/ # Shared UI Component Library
|
│ ├── ui/ # Shared UI Component Library
|
||||||
│ ├── utils/ # Shared Utilities (Date, Encryption, Core Logic, etc)
|
│ ├── utils/ # Shared Utilities (Date, Encryption, Core Logic, etc)
|
||||||
│ └── configs/ # Shared Tooling Configurations
|
│ └── configs/ # Shared Tooling Configurations
|
||||||
@@ -204,7 +205,40 @@ An isolated environment for developing and documenting UI components.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 5. `packages/utils`
|
### 5. `packages/core-api`
|
||||||
|
|
||||||
|
The **platform-agnostic API engine** for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine.
|
||||||
|
|
||||||
|
* Consumed by `apps/web`, `apps/landing`, and any future workspace
|
||||||
|
* Centralizes all `@grafana/faro-*` and `@opentelemetry/*` dependencies
|
||||||
|
* Provides plug-and-play telemetry via `initTelemetry()` + `faroAdapter`
|
||||||
|
|
||||||
|
**Tech Stack**:
|
||||||
|
|
||||||
|
* Axios (isolated instances, zero singleton pollution)
|
||||||
|
* Grafana Faro (RUM, Logs, Error tracking)
|
||||||
|
* OpenTelemetry (custom spans, distributed tracing)
|
||||||
|
* TypeScript (strict types, module augmentation)
|
||||||
|
|
||||||
|
**Key Capabilities**:
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---|---|
|
||||||
|
| 🏭 HTTP Client Factory | `createHttpClient()` — per-app isolated Axios instances with interceptor hooks |
|
||||||
|
| 📡 Faro/Loki Baseline | Every request automatically pushes structured logs with `module.key` and `module.action` |
|
||||||
|
| 🎯 Custom Spans (Opt-In) | `telemetryContext.customSpanName` creates explicit OTel spans visible in Grafana Tempo |
|
||||||
|
| 🛡️ Error Normalization | `ApiError.fromAxiosError()` — structured, serializable error codes for all failure modes |
|
||||||
|
| 📦 Data Services Engine | `CommonRemoteDataServices` — full CRUD + lifecycle operations with zero boilerplate |
|
||||||
|
|
||||||
|
**Documentation**:
|
||||||
|
|
||||||
|
| Document | Contents |
|
||||||
|
|---|---|
|
||||||
|
| [README.md](packages/core-api/README.md) | Architecture, HTTP client setup, observability strategy, data services, app integration guide |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `packages/utils`
|
||||||
|
|
||||||
Shared business logic and reusable utility modules that can be consumed across multiple applications. Fully tested using Vitest.
|
Shared business logic and reusable utility modules that can be consumed across multiple applications. Fully tested using Vitest.
|
||||||
|
|
||||||
@@ -212,7 +246,7 @@ This package is intended to hold non-UI, cross-cutting logic such as date/time h
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 6. `packages/ui`
|
### 7. `packages/ui`
|
||||||
|
|
||||||
Shared UI component library (Buttons, Inputs, Cards, Layouts).
|
Shared UI component library (Buttons, Inputs, Cards, Layouts).
|
||||||
|
|
||||||
@@ -221,7 +255,7 @@ Shared UI component library (Buttons, Inputs, Cards, Layouts).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 7. `packages/configs`
|
### 8. `packages/configs`
|
||||||
|
|
||||||
Single source of truth for tooling configuration.
|
Single source of truth for tooling configuration.
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/core-api": "workspace:*",
|
||||||
"@repo/ui": "workspace:*",
|
"@repo/ui": "workspace:*",
|
||||||
"@repo/utils": "workspace:*",
|
"@repo/utils": "workspace:*",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ThemeProvider } from '@repo/ui/provider';
|
import { ThemeProvider } from '@repo/ui/provider';
|
||||||
import { Button } from '@repo/ui/components';
|
import { Button } from '@repo/ui/components';
|
||||||
|
import LandingSample from './features/public-content/presentation/LandingSample';
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
@@ -31,6 +32,10 @@ export default function App() {
|
|||||||
✅ @repo/ui workspace link verified — Button component rendered successfully.
|
✅ @repo/ui workspace link verified — Button component rendered successfully.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
|
||||||
|
<LandingSample />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
import { publicClient } from '../../../lib/api-client';
|
||||||
|
|
||||||
|
// ─── Domain Entity ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public content entity for the landing page.
|
||||||
|
*
|
||||||
|
* Represents promotional content, blog posts, or announcements
|
||||||
|
* served from a public API without authentication.
|
||||||
|
*/
|
||||||
|
export interface PublicContentEntity extends BaseEntity {
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
excerpt: string;
|
||||||
|
imageUrl: string;
|
||||||
|
publishedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Data Services Instance ─────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Public content data services — wired to the lightweight `publicClient`.
|
||||||
|
*
|
||||||
|
* No auth, no OTel — pure zero-overhead HTTP calls.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const { data } = await publicContentServices.getMany();
|
||||||
|
* const { data: post } = await publicContentServices.getOne('hello-world');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const publicContentServices = new CommonRemoteDataServices<PublicContentEntity>(
|
||||||
|
publicClient,
|
||||||
|
{
|
||||||
|
apiUrl: '/content',
|
||||||
|
moduleKey: 'PUBLIC_CONTENT',
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { publicContentServices } from '../data/public.data-services';
|
||||||
|
import type { PublicContentEntity } from '../data/public.data-services';
|
||||||
|
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||||
|
import { ApiError } from '@repo/core-api/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample component demonstrating the `@repo/core-api` integration
|
||||||
|
* for the Landing App with TelemetryContext escape hatch.
|
||||||
|
*
|
||||||
|
* Pipeline: Faro auto-instrumentation → No Auth → GET /content
|
||||||
|
* + Custom span "public.content.fetch" with enriched tags
|
||||||
|
*/
|
||||||
|
export default function LandingSample() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<ApiResponse<PublicContentEntity[]> | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFetch = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await publicContentServices.getMany<PublicContentEntity[]>({
|
||||||
|
// ── Telemetry Escape Hatch ──────────────────────────────
|
||||||
|
// Even the lightweight landing app can push custom spans
|
||||||
|
// and business events when needed.
|
||||||
|
telemetryContext: {
|
||||||
|
customSpanName: 'public.content.fetch',
|
||||||
|
tags: {
|
||||||
|
'feature': 'landing',
|
||||||
|
'ui.component': 'LandingSample',
|
||||||
|
'ui.action': 'content_fetch',
|
||||||
|
},
|
||||||
|
pushEventOnSuccess: 'public_content_loaded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setResult(response);
|
||||||
|
console.log('[LandingSample] Response:', response);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||||
|
console.error('[LandingSample] ApiError:', err.toJSON());
|
||||||
|
} else {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, fontFamily: 'monospace' }}>
|
||||||
|
<h2>🧪 Public Content Services — Integration Test</h2>
|
||||||
|
<p style={{ color: '#888', fontSize: 14 }}>
|
||||||
|
Pipeline: Faro + Custom Span "public.content.fetch" → No Auth → GET /content
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleFetch}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: 16,
|
||||||
|
cursor: loading ? 'wait' : 'pointer',
|
||||||
|
background: loading ? '#555' : '#059669',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Fetching…' : 'Test Public Fetch'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<pre style={{ color: '#ef4444', marginTop: 16, whiteSpace: 'pre-wrap' }}>
|
||||||
|
❌ {error}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<pre style={{ marginTop: 16, background: '#1e1e2e', color: '#a6e3a1', padding: 16, borderRadius: 8, overflow: 'auto' }}>
|
||||||
|
{JSON.stringify(result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { createHttpClient } from '@repo/core-api/http-client';
|
||||||
|
import { faroAdapter } from '@repo/core-api/observability';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight public HTTP client for `apps/landing`.
|
||||||
|
*
|
||||||
|
* - Uses the shared Faro adapter (initialized via `initTelemetry()` in main.tsx)
|
||||||
|
* - No auth token injection
|
||||||
|
* - Minimal config for maximum performance
|
||||||
|
*
|
||||||
|
* Designed for public-facing content that requires no authentication.
|
||||||
|
*/
|
||||||
|
export const publicClient = createHttpClient({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL ?? 'https://api.eigen.co/public/v1',
|
||||||
|
timeout: 10000,
|
||||||
|
observability: faroAdapter,
|
||||||
|
});
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
// ─── Optional Telemetry Bootstrap ───────────────────────────────
|
||||||
|
// Landing app uses minimal telemetry. Remove this block entirely
|
||||||
|
// if you want zero observability overhead.
|
||||||
|
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||||
|
|
||||||
|
initTelemetry({
|
||||||
|
appName: import.meta.env.VITE_APP_NAME || 'fe-monorepo-landing',
|
||||||
|
appVersion: import.meta.env.VITE_APP_VERSION || '0.0.0',
|
||||||
|
telemetryUrl: import.meta.env.VITE_FARO_URL || 'https://telemetry.eigen.co.id/collect',
|
||||||
|
environment: import.meta.env.VITE_ENV || 'development',
|
||||||
|
// No otlpTraceUrl — only Faro collector, minimal overhead
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Application Bootstrap ──────────────────────────────────────
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
|
|||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@repo/core-api": "workspace:*",
|
||||||
"@repo/ui": "workspace:*",
|
"@repo/ui": "workspace:*",
|
||||||
"@repo/utils": "workspace:*",
|
"@repo/utils": "workspace:*",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ export default function App() {
|
|||||||
<Route path="/403" element={<Forbidden />} />
|
<Route path="/403" element={<Forbidden />} />
|
||||||
<Route path="/maintenance" element={<Maintenance />} />
|
<Route path="/maintenance" element={<Maintenance />} />
|
||||||
<Route path="/coming-soon" element={<ComingSoon />} />
|
<Route path="/coming-soon" element={<ComingSoon />} />
|
||||||
<Route path="/" element={<Navigate to="/showcase" />} />
|
{/* <Route path="/" element={<Navigate to="/showcase" />} /> */}
|
||||||
|
<Route path="/" element={<Navigate to="/app" />} />
|
||||||
<Route path="*" element={<Navigate to="/404" />} />
|
<Route path="*" element={<Navigate to="/404" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import BookingSample from "./features/booking/presentation/BookingSample";
|
||||||
|
|
||||||
export default function ExamplePage() {
|
export default function ExamplePage() {
|
||||||
return <div className="bg-amber-200">example</div>;
|
return <div className="bg-amber-200">example
|
||||||
|
<div className="p-8">
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Enterprise Web App</h1>
|
||||||
|
<BookingSample />
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
import { apiClient } from '../../../../../../lib/api-client';
|
||||||
|
|
||||||
|
// ─── Domain Entity ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking domain entity.
|
||||||
|
*
|
||||||
|
* In a real module, this would be defined in the domain layer
|
||||||
|
* (e.g., `features/booking/domain/entities.ts`) and imported here.
|
||||||
|
*/
|
||||||
|
export interface BookingEntity extends BaseEntity {
|
||||||
|
bookingCode: string;
|
||||||
|
customerName: string;
|
||||||
|
checkInDate: string;
|
||||||
|
checkOutDate: string;
|
||||||
|
status: 'pending' | 'confirmed' | 'cancelled';
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Data Services Instance ─────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Booking data services — wired to the enterprise `apiClient`.
|
||||||
|
*
|
||||||
|
* All requests flow through the full interceptor chain:
|
||||||
|
* Faro tracing → Bearer token injection → ApiError normalization.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const { data } = await bookingServices.getMany({ params: { page: 1 } });
|
||||||
|
* const { data: booking } = await bookingServices.getOne('42');
|
||||||
|
* await bookingServices.confirmProcessTransaction('42');
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||||
|
apiClient,
|
||||||
|
{
|
||||||
|
apiUrl: '/bookings',
|
||||||
|
moduleKey: 'BOOKING',
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { bookingServices } from '../data/booking.data-services';
|
||||||
|
import type { BookingEntity } from '../data/booking.data-services';
|
||||||
|
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||||
|
import { ApiError } from '@repo/core-api/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sample component demonstrating `@repo/core-api` integration
|
||||||
|
* with the advanced TelemetryContext escape hatch.
|
||||||
|
*
|
||||||
|
* Pipeline: Faro auto-instrumentation → Bearer token → GET /bookings
|
||||||
|
* + Custom span "booking.list.fetch" with enriched tags
|
||||||
|
*/
|
||||||
|
export default function BookingSample() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [result, setResult] = useState<ApiResponse<BookingEntity[]> | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFetch = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
setResult(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await bookingServices.getMany<BookingEntity[]>({
|
||||||
|
params: { page: 1, limit: 20 },
|
||||||
|
// ── Telemetry Escape Hatch ──────────────────────────────
|
||||||
|
// This creates a custom OTel span named "booking.list.fetch",
|
||||||
|
// attaches business tags, and pushes a Faro event on success.
|
||||||
|
telemetryContext: {
|
||||||
|
customSpanName: 'booking.list.fetch',
|
||||||
|
tags: {
|
||||||
|
'feature': 'booking',
|
||||||
|
'ui.component': 'BookingSample',
|
||||||
|
'ui.action': 'list_fetch',
|
||||||
|
'page': 1,
|
||||||
|
},
|
||||||
|
pushEventOnSuccess: 'booking_list_loaded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setResult(response);
|
||||||
|
console.log('[BookingSample] Response:', response);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||||
|
console.error('[BookingSample] ApiError:', err.toJSON());
|
||||||
|
} else {
|
||||||
|
setError(err instanceof Error ? err.message : 'Unknown error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 24, fontFamily: 'monospace' }}>
|
||||||
|
<h2>🧪 Booking Data Services — Integration Test</h2>
|
||||||
|
<p style={{ color: '#888', fontSize: 14 }}>
|
||||||
|
Pipeline: Faro + Custom Span "booking.list.fetch" → Bearer Token → GET /bookings
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleFetch}
|
||||||
|
disabled={loading}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
fontSize: 16,
|
||||||
|
cursor: loading ? 'wait' : 'pointer',
|
||||||
|
background: loading ? '#555' : '#4f46e5',
|
||||||
|
color: '#fff',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: 6,
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{loading ? 'Fetching…' : 'Test Fetch Bookings'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<pre style={{ color: '#ef4444', marginTop: 16, whiteSpace: 'pre-wrap' }}>
|
||||||
|
❌ {error}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<pre style={{ marginTop: 16, background: '#1e1e2e', color: '#a6e3a1', padding: 16, borderRadius: 8, overflow: 'auto' }}>
|
||||||
|
{JSON.stringify(result, null, 2)}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { createHttpClient } from '@repo/core-api/http-client';
|
||||||
|
import { faroAdapter } from '@repo/core-api/observability';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enterprise HTTP client for `apps/web`.
|
||||||
|
*
|
||||||
|
* - Full Faro observability via the shared `faroAdapter`
|
||||||
|
* - Automatic Bearer token injection from localStorage
|
||||||
|
* - 401 redirect to `/auth/login`
|
||||||
|
* - Supports per-request `telemetryContext` for custom spans/tags
|
||||||
|
*
|
||||||
|
* All interceptors (auth, observability, error normalization)
|
||||||
|
* are baked into this instance. Import this singleton throughout
|
||||||
|
* the web application — never create raw axios instances.
|
||||||
|
*/
|
||||||
|
export const apiClient = createHttpClient(
|
||||||
|
{
|
||||||
|
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||||
|
timeout: 15000,
|
||||||
|
observability: faroAdapter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// ── Auth Interceptor ──────────────────────────────────────────
|
||||||
|
onRequest: async (config) => {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Error Interceptor ─────────────────────────────────────────
|
||||||
|
onResponseError: async (error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
// Clear stale token and redirect to login
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
+14
-7
@@ -1,15 +1,22 @@
|
|||||||
// import { DateUtils } from '@repo/utils';
|
// ─── Observability Bootstrap (MUST be first) ────────────────────
|
||||||
// import { CurrencyUtils } from '@repo/utils';
|
// Initializes Grafana Faro + OTel auto-instrumentation before any
|
||||||
|
// React code or HTTP requests are executed.
|
||||||
|
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',
|
||||||
|
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||||
|
environment: import.meta.env.VITE_ENV || 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Application Bootstrap ──────────────────────────────────────
|
||||||
import './main.css';
|
import './main.css';
|
||||||
import { lazy, StrictMode } from 'react';
|
import { lazy, StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
const App = lazy(() => import('./apps'));
|
const App = lazy(() => import('./apps'));
|
||||||
|
|
||||||
// DateUtils.setGlobalConfig('Asia/Jakarta');
|
|
||||||
// DateUtils.setGlobalConfig('Asia/Makassar');
|
|
||||||
|
|
||||||
// CurrencyUtils.setGlobalPrefix('IDR ');
|
|
||||||
// CurrencyUtils.setGlobalDecimalSeparator(',');
|
|
||||||
createRoot(document.getElementById('app')!).render(
|
createRoot(document.getElementById('app')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
# @repo/core-api
|
||||||
|
|
||||||
|
The platform-agnostic API engine for the monorepo. Provides an isolated HTTP client factory, a unified observability pipeline (Grafana Faro + OpenTelemetry), and a generic data services engine — consumed by `apps/web`, `apps/landing`, and any future workspace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Architecture Overview](#architecture-overview)
|
||||||
|
- [HTTP Client](#http-client)
|
||||||
|
- [Observability](#observability)
|
||||||
|
- [Data Services](#data-services)
|
||||||
|
- [Application Setup Guide](#application-setup-guide)
|
||||||
|
- [Per-Request Telemetry (Escape Hatch)](#per-request-telemetry-escape-hatch)
|
||||||
|
- [Error Handling](#error-handling)
|
||||||
|
- [Package Exports](#package-exports)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ @repo/core-api │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ http-client │ │ observability │ │ data-services │ │
|
||||||
|
│ │ │ │ │ │ │ │
|
||||||
|
│ │ createHttp │◄──│ faroAdapter │ │ BaseRemoteData │ │
|
||||||
|
│ │ Client() │ │ initTelemetry() │ │ Services │ │
|
||||||
|
│ │ │ │ getFaro() │ │ CommonRemoteData │ │
|
||||||
|
│ │ ApiResponse │ │ noopAdapter │ │ Services │ │
|
||||||
|
│ └──────┬───────┘ └───────────────────┘ └────────┬──────────┘ │
|
||||||
|
│ │ │ │
|
||||||
|
│ └────────────────────┬───────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────┴────────┐ │
|
||||||
|
│ │ errors │ │
|
||||||
|
│ │ ApiError │ │
|
||||||
|
│ │ ErrorCodes │ │
|
||||||
|
│ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
│ │ │
|
||||||
|
▼ ▼ ▼
|
||||||
|
apps/web apps/landing apps/desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow
|
||||||
|
|
||||||
|
Every HTTP request flows through this pipeline:
|
||||||
|
|
||||||
|
```
|
||||||
|
Component → DataService.getMany() → execute()
|
||||||
|
→ httpClient.request()
|
||||||
|
→ Request Interceptor:
|
||||||
|
1. faroAdapter.onRequestStart() ← Faro log + optional custom span
|
||||||
|
2. hooks.onRequest() ← App-specific (e.g., Bearer token)
|
||||||
|
→ Network (fetch/XHR)
|
||||||
|
→ Response Interceptor:
|
||||||
|
SUCCESS: faroAdapter.onRequestEnd() → hooks.onResponse()
|
||||||
|
ERROR: faroAdapter.onRequestError() → hooks.onResponseError()
|
||||||
|
→ ApiError.fromAxiosError()
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> Observability adapter errors are **caught internally** via try-catch in the interceptor chain. An adapter crash will never swallow or replace the original API error — the UI always receives the correct rejection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## HTTP Client
|
||||||
|
|
||||||
|
### `createHttpClient(config, hooks?)`
|
||||||
|
|
||||||
|
Creates an **isolated** Axios instance. Each app receives its own interceptor chain — no globals are shared or mutated.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { createHttpClient } from '@repo/core-api/http-client';
|
||||||
|
import { faroAdapter } from '@repo/core-api/observability';
|
||||||
|
|
||||||
|
export const apiClient = createHttpClient(
|
||||||
|
{
|
||||||
|
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||||
|
timeout: 15000,
|
||||||
|
observability: faroAdapter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onRequest: async (config) => {
|
||||||
|
const token = localStorage.getItem('access_token');
|
||||||
|
if (token) config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
onResponseError: async (error) => {
|
||||||
|
if (error.response?.status === 401) {
|
||||||
|
localStorage.removeItem('access_token');
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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) |
|
||||||
|
|
||||||
|
### 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) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Observability
|
||||||
|
|
||||||
|
### Strategy: Opt-In Custom Spans + Faro/Loki Baseline
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|
||||||
|
> [!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
|
||||||
|
|
||||||
|
Call `initTelemetry()` **once** at the top of your app's entry point, before any React code:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||||
|
|
||||||
|
initTelemetry({
|
||||||
|
appName: 'fe-monorepo-web',
|
||||||
|
appVersion: '1.0.0',
|
||||||
|
telemetryUrl: 'https://telemetry.eigen.co.id/collect',
|
||||||
|
environment: 'production',
|
||||||
|
// Optional: direct OTLP export to Grafana Tempo
|
||||||
|
otlpTraceUrl: 'https://telemetry.eigen.co.id/v1/traces',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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: `[/.*/]`) |
|
||||||
|
|
||||||
|
### 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`) |
|
||||||
|
|
||||||
|
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)**.
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Services
|
||||||
|
|
||||||
|
### `CommonRemoteDataServices<E>`
|
||||||
|
|
||||||
|
A concrete, ready-to-use data services class that provides full CRUD and lifecycle operations. Extends `BaseRemoteDataServices<E>`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
|
||||||
|
interface BookingEntity extends BaseEntity {
|
||||||
|
bookingCode: string;
|
||||||
|
customerName: string;
|
||||||
|
status: 'pending' | 'confirmed' | 'cancelled';
|
||||||
|
}
|
||||||
|
|
||||||
|
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 |
|
||||||
|
|
||||||
|
All batch variants (`batchActivate`, `batchDeactivate`, etc.) are also available.
|
||||||
|
|
||||||
|
### Escape Hatch: `customRequest<T>(config)`
|
||||||
|
|
||||||
|
For non-standard endpoints that don't fit the CRUD pattern:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const taxResult = await bookingServices.customRequest<TaxCalculation>({
|
||||||
|
url: '/bookings/42/calculate-tax',
|
||||||
|
method: 'POST',
|
||||||
|
data: { items: [...] },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Application Setup Guide
|
||||||
|
|
||||||
|
### 1. Initialize Telemetry (Entry Point)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/main.tsx — MUST be the first import
|
||||||
|
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',
|
||||||
|
otlpTraceUrl: import.meta.env.VITE_OTLP_TRACE_URL || 'https://telemetry.eigen.co.id/v1/traces',
|
||||||
|
environment: import.meta.env.VITE_ENV || 'development',
|
||||||
|
});
|
||||||
|
|
||||||
|
// ... rest of React bootstrap
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Create the HTTP Client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// apps/web/src/lib/api-client.ts
|
||||||
|
import { createHttpClient } from '@repo/core-api/http-client';
|
||||||
|
import { faroAdapter } from '@repo/core-api/observability';
|
||||||
|
|
||||||
|
export const apiClient = createHttpClient({
|
||||||
|
baseURL: import.meta.env.VITE_API_URL ?? 'http://localhost:8080/api/v1',
|
||||||
|
timeout: 15000,
|
||||||
|
observability: faroAdapter,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Create a Data Service
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// features/booking/data/booking.data-services.ts
|
||||||
|
import { CommonRemoteDataServices } from '@repo/core-api/data-services';
|
||||||
|
import type { BaseEntity } from '@repo/core-api/data-services';
|
||||||
|
import { apiClient } from '@/lib/api-client';
|
||||||
|
|
||||||
|
export interface BookingEntity extends BaseEntity {
|
||||||
|
bookingCode: string;
|
||||||
|
customerName: string;
|
||||||
|
status: 'pending' | 'confirmed' | 'cancelled';
|
||||||
|
totalAmount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||||
|
apiClient,
|
||||||
|
{ apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Consume in a React Component
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { bookingServices } from '../data/booking.data-services';
|
||||||
|
import type { BookingEntity } from '../data/booking.data-services';
|
||||||
|
import type { ApiResponse } from '@repo/core-api/http-client';
|
||||||
|
import { ApiError } from '@repo/core-api/errors';
|
||||||
|
|
||||||
|
export default function BookingSample() {
|
||||||
|
const [result, setResult] = useState<ApiResponse<BookingEntity[]> | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const handleFetch = async () => {
|
||||||
|
try {
|
||||||
|
const response = await bookingServices.getMany<BookingEntity[]>({
|
||||||
|
params: { page: 1, limit: 20 },
|
||||||
|
// Optional: Per-request telemetry escape hatch
|
||||||
|
telemetryContext: {
|
||||||
|
customSpanName: 'booking.list.fetch',
|
||||||
|
tags: { feature: 'booking', page: 1 },
|
||||||
|
pushEventOnSuccess: 'booking_list_loaded',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setResult(response);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
setError(`[${err.code}] ${err.message} (HTTP ${err.status})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <button onClick={handleFetch}>Fetch Bookings</button>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-Request Telemetry (Escape Hatch)
|
||||||
|
|
||||||
|
### `TelemetryContext`
|
||||||
|
|
||||||
|
Attach to any request via the `telemetryContext` property to push custom spans and business events:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TelemetryContext {
|
||||||
|
/** Creates a custom OTel span wrapping this request (visible in Grafana Tempo). */
|
||||||
|
customSpanName?: string;
|
||||||
|
/** Custom tags enriching the span and Faro logs (prefixed with `custom.` on spans). */
|
||||||
|
tags?: Record<string, string | number | boolean>;
|
||||||
|
/** Pushes a named Faro event on success (visible in Grafana Faro dashboard). */
|
||||||
|
pushEventOnSuccess?: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Precedence
|
||||||
|
|
||||||
|
`telemetryContext` can be provided at two levels. The top-level `ExecuteOptions.telemetryContext` takes precedence over `config.telemetryContext`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Top-level (preferred)
|
||||||
|
await bookingServices.getMany({
|
||||||
|
telemetryContext: { customSpanName: 'booking.list.fetch' },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Nested in config (also works)
|
||||||
|
await bookingServices.getMany({
|
||||||
|
params: { page: 1 },
|
||||||
|
telemetryContext: { customSpanName: 'booking.list.fetch' },
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### `ApiError`
|
||||||
|
|
||||||
|
All non-2xx responses are normalized into structured `ApiError` instances:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
try {
|
||||||
|
await bookingServices.getOne('42');
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ApiError) {
|
||||||
|
err.code; // ApiErrorCode.NOT_FOUND
|
||||||
|
err.status; // 404
|
||||||
|
err.message; // "Booking not found"
|
||||||
|
err.data; // Raw server response body
|
||||||
|
err.toJSON(); // Serializable for logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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` |
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/core-api",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
"./http-client": "./src/http-client/index.ts",
|
||||||
|
"./observability": "./src/observability/index.ts",
|
||||||
|
"./observability/setup": "./src/observability/setup.ts",
|
||||||
|
"./data-services": "./src/data-services/index.ts",
|
||||||
|
"./errors": "./src/errors/index.ts"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"lint": "eslint \"**/*.ts\"",
|
||||||
|
"test": "vitest run",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@grafana/faro-react": "^2.1.0",
|
||||||
|
"@grafana/faro-web-sdk": "^2.1.0",
|
||||||
|
"@grafana/faro-web-tracing": "^2.1.0",
|
||||||
|
"@opentelemetry/api": "^1.9.0",
|
||||||
|
"@opentelemetry/exporter-trace-otlp-http": "^0.213.0",
|
||||||
|
"@opentelemetry/sdk-trace-web": "^2.2.0",
|
||||||
|
"axios": "^1.9.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/eslint-config": "workspace:*",
|
||||||
|
"@repo/typescript-config": "workspace:*",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"typescript": "5.5.4",
|
||||||
|
"vitest": "^4.0.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { BaseEntity } from './types';
|
||||||
|
import { BaseRemoteDataServices } from './base-remote.data-services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* General-purpose remote data services.
|
||||||
|
*
|
||||||
|
* A concrete, non-abstract version of BaseRemoteDataServices that
|
||||||
|
* can be instantiated directly for standard CRUD modules that don't
|
||||||
|
* need additional custom methods.
|
||||||
|
*
|
||||||
|
* For modules requiring domain-specific operations beyond standard
|
||||||
|
* CRUD + lifecycle, extend BaseRemoteDataServices instead and add
|
||||||
|
* custom methods using `this.execute()` or `this.customRequest()`.
|
||||||
|
*
|
||||||
|
* @typeParam E - The domain entity type
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Direct instantiation for standard modules
|
||||||
|
* const bookingServices = new CommonRemoteDataServices<BookingEntity>(
|
||||||
|
* apiClient,
|
||||||
|
* { apiUrl: '/bookings', moduleKey: 'BOOKING' },
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* const { data } = await bookingServices.getMany();
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // For modules needing custom operations, extend the base:
|
||||||
|
* class InvoiceDataServices extends BaseRemoteDataServices<InvoiceEntity> {
|
||||||
|
* async calculateTax(invoiceId: string) {
|
||||||
|
* return this.customRequest<TaxResult>({
|
||||||
|
* url: `/invoices/${invoiceId}/calculate-tax`,
|
||||||
|
* method: 'POST',
|
||||||
|
* });
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class CommonRemoteDataServices<
|
||||||
|
E extends BaseEntity = BaseEntity,
|
||||||
|
> extends BaseRemoteDataServices<E> {}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import type { RequestMethodMap, RequestDescriptor, ApiURLMap } from './types';
|
||||||
|
|
||||||
|
// ─── Request Actions ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constants for the 'ex-module-action' header.
|
||||||
|
* Maps to backend permission/audit checks.
|
||||||
|
*/
|
||||||
|
export const REQUEST_ACTION = {
|
||||||
|
VIEW: 'VIEW',
|
||||||
|
CREATE: 'CREATE',
|
||||||
|
EDIT: 'EDIT',
|
||||||
|
DELETE: 'DELETE',
|
||||||
|
CONFIRM_DATA: 'CONFIRM_DATA',
|
||||||
|
CANCEL_DATA: 'CANCEL_DATA',
|
||||||
|
CONFIRM_PROCESS_TRANSACTION: 'CONFIRM_PROCESS_TRANSACTION',
|
||||||
|
CANCEL_PROCESS_TRANSACTION: 'CANCEL_PROCESS_TRANSACTION',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// ─── Default HTTP Methods ───────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sensible REST defaults for all operations.
|
||||||
|
* Can be overridden per data-services instance.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_METHODS: RequestMethodMap = {
|
||||||
|
getManyMethod: 'GET',
|
||||||
|
getOneMethod: 'GET',
|
||||||
|
createMethod: 'POST',
|
||||||
|
editMethod: 'PUT',
|
||||||
|
deleteMethod: 'DELETE',
|
||||||
|
batchDeleteMethod: 'POST',
|
||||||
|
|
||||||
|
activateMethod: 'PATCH',
|
||||||
|
batchActivateMethod: 'POST',
|
||||||
|
deactivateMethod: 'PATCH',
|
||||||
|
batchDeactivateMethod: 'POST',
|
||||||
|
|
||||||
|
confirmProcessDataMethod: 'PATCH',
|
||||||
|
batchConfirmProcessDataMethod: 'POST',
|
||||||
|
cancelProcessDataMethod: 'PATCH',
|
||||||
|
batchCancelProcessDataMethod: 'POST',
|
||||||
|
|
||||||
|
confirmProcessTransactionMethod: 'PATCH',
|
||||||
|
batchConfirmProcessTransactionMethod: 'POST',
|
||||||
|
cancelProcessTransactionMethod: 'PATCH',
|
||||||
|
batchCancelProcessTransactionMethod: 'POST',
|
||||||
|
rollbackProcessTransactionMethod: 'PATCH',
|
||||||
|
batchRollbackProcessTransactionMethod: 'POST',
|
||||||
|
holdProcessTransactionMethod: 'PATCH',
|
||||||
|
batchHoldProcessTransactionMethod: 'POST',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── Operation Descriptors ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pre-defined descriptors for all standard operations.
|
||||||
|
* Each descriptor maps an operation to its URL key, method key,
|
||||||
|
* and action header — eliminating 22 boilerplate methods.
|
||||||
|
*/
|
||||||
|
export const DESCRIPTORS = {
|
||||||
|
getMany: { urlKey: 'getManyUrl', methodKey: 'getManyMethod', action: REQUEST_ACTION.VIEW },
|
||||||
|
getOne: { urlKey: 'getOneUrl', methodKey: 'getOneMethod', action: REQUEST_ACTION.VIEW },
|
||||||
|
create: { urlKey: 'createUrl', methodKey: 'createMethod', action: REQUEST_ACTION.CREATE },
|
||||||
|
edit: { urlKey: 'editUrl', methodKey: 'editMethod', action: REQUEST_ACTION.EDIT },
|
||||||
|
delete: { urlKey: 'deleteUrl', methodKey: 'deleteMethod', action: REQUEST_ACTION.DELETE },
|
||||||
|
batchDelete: { urlKey: 'batchDeleteUrl', methodKey: 'batchDeleteMethod', action: REQUEST_ACTION.DELETE },
|
||||||
|
|
||||||
|
activate: { urlKey: 'activateUrl', methodKey: 'activateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
batchActivate: { urlKey: 'batchActivateUrl', methodKey: 'batchActivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
deactivate: { urlKey: 'deactivateUrl', methodKey: 'deactivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
batchDeactivate: { urlKey: 'batchDeactivateUrl', methodKey: 'batchDeactivateMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
|
||||||
|
confirmProcessData: { urlKey: 'confirmProcessDataUrl', methodKey: 'confirmProcessDataMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
batchConfirmProcessData: { urlKey: 'batchConfirmProcessDataUrl', methodKey: 'batchConfirmProcessDataMethod', action: REQUEST_ACTION.CONFIRM_DATA },
|
||||||
|
cancelProcessData: { urlKey: 'cancelProcessDataUrl', methodKey: 'cancelProcessDataMethod', action: REQUEST_ACTION.CANCEL_DATA },
|
||||||
|
batchCancelProcessData: { urlKey: 'batchCancelProcessDataUrl', methodKey: 'batchCancelProcessDataMethod', action: REQUEST_ACTION.CANCEL_DATA },
|
||||||
|
|
||||||
|
confirmProcessTransaction: { urlKey: 'confirmProcessTransactionUrl', methodKey: 'confirmProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
batchConfirmProcessTransaction: { urlKey: 'batchConfirmProcessTransactionUrl', methodKey: 'batchConfirmProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
cancelProcessTransaction: { urlKey: 'cancelProcessTransactionUrl', methodKey: 'cancelProcessTransactionMethod', action: REQUEST_ACTION.CANCEL_PROCESS_TRANSACTION },
|
||||||
|
batchCancelProcessTransaction: { urlKey: 'batchCancelProcessTransactionUrl', methodKey: 'batchCancelProcessTransactionMethod', action: REQUEST_ACTION.CANCEL_PROCESS_TRANSACTION },
|
||||||
|
rollbackProcessTransaction: { urlKey: 'rollbackProcessTransactionUrl', methodKey: 'rollbackProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
batchRollbackProcessTransaction: { urlKey: 'batchRollbackProcessTransactionUrl', methodKey: 'batchRollbackProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
holdProcessTransaction: { urlKey: 'holdProcessTransactionUrl', methodKey: 'holdProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
batchHoldProcessTransaction: { urlKey: 'batchHoldProcessTransactionUrl', methodKey: 'batchHoldProcessTransactionMethod', action: REQUEST_ACTION.CONFIRM_PROCESS_TRANSACTION },
|
||||||
|
} as const satisfies Record<string, RequestDescriptor>;
|
||||||
|
|
||||||
|
// ─── Default URL Factory ────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates the complete API URL map from a base path.
|
||||||
|
*
|
||||||
|
* @param apiUrl - Base API path (e.g., '/bookings')
|
||||||
|
* @returns Full ApiURLMap with all CRUD + lifecycle URLs
|
||||||
|
*/
|
||||||
|
export function makeDefaultURLs(apiUrl: string): ApiURLMap {
|
||||||
|
return {
|
||||||
|
getManyUrl: `${apiUrl}`,
|
||||||
|
getOneUrl: `${apiUrl}/:id`,
|
||||||
|
createUrl: `${apiUrl}`,
|
||||||
|
editUrl: `${apiUrl}/:id`,
|
||||||
|
|
||||||
|
deleteUrl: `${apiUrl}/:id`,
|
||||||
|
batchDeleteUrl: `${apiUrl}/batch-delete`,
|
||||||
|
|
||||||
|
activateUrl: `${apiUrl}/:id/active`,
|
||||||
|
batchActivateUrl: `${apiUrl}/batch-active`,
|
||||||
|
deactivateUrl: `${apiUrl}/:id/inactive`,
|
||||||
|
batchDeactivateUrl: `${apiUrl}/batch-inactive`,
|
||||||
|
|
||||||
|
confirmProcessDataUrl: `${apiUrl}/:id/confirm`,
|
||||||
|
batchConfirmProcessDataUrl: `${apiUrl}/batch-confirm`,
|
||||||
|
cancelProcessDataUrl: `${apiUrl}/:id/cancel`,
|
||||||
|
batchCancelProcessDataUrl: `${apiUrl}/batch-cancel`,
|
||||||
|
|
||||||
|
confirmProcessTransactionUrl: `${apiUrl}/:id/confirm-data`,
|
||||||
|
batchConfirmProcessTransactionUrl: `${apiUrl}/batch-confirm-data`,
|
||||||
|
cancelProcessTransactionUrl: `${apiUrl}/:id/cancel`,
|
||||||
|
batchCancelProcessTransactionUrl: `${apiUrl}/batch-cancel`,
|
||||||
|
rollbackProcessTransactionUrl: `${apiUrl}/:id/confirm-rollback`,
|
||||||
|
batchRollbackProcessTransactionUrl: `${apiUrl}/batch-confirm-rollback`,
|
||||||
|
holdProcessTransactionUrl: `${apiUrl}/:id/confirm-hold`,
|
||||||
|
batchHoldProcessTransactionUrl: `${apiUrl}/batch-confirm-hold`,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// ─── Classes ────────────────────────────────────────────────────
|
||||||
|
export { BaseRemoteDataServices } from './base-remote.data-services';
|
||||||
|
export { CommonRemoteDataServices } from './common-remote.data-services';
|
||||||
|
|
||||||
|
// ─── Utilities ──────────────────────────────────────────────────
|
||||||
|
export { interpolateUrl } from './url-builder';
|
||||||
|
|
||||||
|
// ─── Constants ──────────────────────────────────────────────────
|
||||||
|
export { REQUEST_ACTION, DEFAULT_METHODS, DESCRIPTORS, makeDefaultURLs } from './constants';
|
||||||
|
|
||||||
|
// ─── Types ──────────────────────────────────────────────────────
|
||||||
|
export type {
|
||||||
|
BaseEntity,
|
||||||
|
ApiURLMap,
|
||||||
|
RequestMethodMap,
|
||||||
|
RequestDescriptor,
|
||||||
|
ExecuteOptions,
|
||||||
|
DataServicesConfig,
|
||||||
|
} from './types';
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import type { TelemetryContext } from '../http-client/types';
|
||||||
|
|
||||||
|
// ─── Base Entity ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal entity contract. All domain entities must have
|
||||||
|
* an optional `id` field for CRUD operations.
|
||||||
|
*/
|
||||||
|
export interface BaseEntity {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── API URL Map ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete URL map for all standard CRUD and lifecycle operations.
|
||||||
|
* Each key maps to a URL template string (e.g., '/bookings/:id').
|
||||||
|
*/
|
||||||
|
export interface ApiURLMap {
|
||||||
|
getManyUrl: string;
|
||||||
|
getOneUrl: string;
|
||||||
|
createUrl: string;
|
||||||
|
editUrl: string;
|
||||||
|
deleteUrl: string;
|
||||||
|
batchDeleteUrl: string;
|
||||||
|
|
||||||
|
activateUrl: string;
|
||||||
|
batchActivateUrl: string;
|
||||||
|
deactivateUrl: string;
|
||||||
|
batchDeactivateUrl: string;
|
||||||
|
|
||||||
|
confirmProcessDataUrl: string;
|
||||||
|
batchConfirmProcessDataUrl: string;
|
||||||
|
cancelProcessDataUrl: string;
|
||||||
|
batchCancelProcessDataUrl: string;
|
||||||
|
|
||||||
|
confirmProcessTransactionUrl: string;
|
||||||
|
batchConfirmProcessTransactionUrl: string;
|
||||||
|
cancelProcessTransactionUrl: string;
|
||||||
|
batchCancelProcessTransactionUrl: string;
|
||||||
|
rollbackProcessTransactionUrl: string;
|
||||||
|
batchRollbackProcessTransactionUrl: string;
|
||||||
|
holdProcessTransactionUrl: string;
|
||||||
|
batchHoldProcessTransactionUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── HTTP Method Map ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP method overrides for each operation.
|
||||||
|
* Defaults to sensible REST conventions (GET, POST, PUT, DELETE, PATCH).
|
||||||
|
*/
|
||||||
|
export interface RequestMethodMap {
|
||||||
|
getManyMethod: string;
|
||||||
|
getOneMethod: string;
|
||||||
|
createMethod: string;
|
||||||
|
editMethod: string;
|
||||||
|
deleteMethod: string;
|
||||||
|
batchDeleteMethod: string;
|
||||||
|
|
||||||
|
activateMethod: string;
|
||||||
|
batchActivateMethod: string;
|
||||||
|
deactivateMethod: string;
|
||||||
|
batchDeactivateMethod: string;
|
||||||
|
|
||||||
|
confirmProcessDataMethod: string;
|
||||||
|
batchConfirmProcessDataMethod: string;
|
||||||
|
cancelProcessDataMethod: string;
|
||||||
|
batchCancelProcessDataMethod: string;
|
||||||
|
|
||||||
|
confirmProcessTransactionMethod: string;
|
||||||
|
batchConfirmProcessTransactionMethod: string;
|
||||||
|
cancelProcessTransactionMethod: string;
|
||||||
|
batchCancelProcessTransactionMethod: string;
|
||||||
|
rollbackProcessTransactionMethod: string;
|
||||||
|
batchRollbackProcessTransactionMethod: string;
|
||||||
|
holdProcessTransactionMethod: string;
|
||||||
|
batchHoldProcessTransactionMethod: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Request Descriptor ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes a single operation in terms of its URL, method, and
|
||||||
|
* action header. Used by the generic `execute()` method.
|
||||||
|
*/
|
||||||
|
export interface RequestDescriptor {
|
||||||
|
/** Key into the ApiURLMap. */
|
||||||
|
urlKey: keyof ApiURLMap;
|
||||||
|
/** Key into the RequestMethodMap. */
|
||||||
|
methodKey: keyof RequestMethodMap;
|
||||||
|
/** Value for the 'ex-module-action' header. */
|
||||||
|
action: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Execute Options ────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options passed to the generic `execute()` method.
|
||||||
|
*/
|
||||||
|
export interface ExecuteOptions {
|
||||||
|
/** Dynamic URL parameters (e.g., `{ id: '42' }`). */
|
||||||
|
variableURL?: Record<string, string>;
|
||||||
|
/** Additional Axios request config (params, data, headers, etc). */
|
||||||
|
config?: AxiosRequestConfig;
|
||||||
|
/** Per-request telemetry context for custom spans, tags, events. */
|
||||||
|
telemetryContext?: TelemetryContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Data Services Constructor ──────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for constructing a BaseRemoteDataServices instance.
|
||||||
|
*/
|
||||||
|
export interface DataServicesConfig {
|
||||||
|
/** Base API path (e.g., '/bookings'). Used to generate all URL templates. */
|
||||||
|
apiUrl?: string;
|
||||||
|
/** Module key for the 'ex-module-key' header (e.g., 'BOOKING'). */
|
||||||
|
moduleKey?: string;
|
||||||
|
/** Override specific URL templates. */
|
||||||
|
urls?: Partial<ApiURLMap>;
|
||||||
|
/** Override specific HTTP methods. */
|
||||||
|
methods?: Partial<RequestMethodMap>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* Interpolates dynamic URL parameters.
|
||||||
|
*
|
||||||
|
* Replaces `:paramName` segments in a URL template with values
|
||||||
|
* from the provided variables object.
|
||||||
|
*
|
||||||
|
* @param template - URL template (e.g., '/bookings/:id/confirm')
|
||||||
|
* @param variables - Key-value map of parameter names to values
|
||||||
|
* @returns The interpolated URL string
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* interpolateUrl('/bookings/:id/confirm', { id: '42' });
|
||||||
|
* // => '/bookings/42/confirm'
|
||||||
|
*
|
||||||
|
* interpolateUrl('/orgs/:orgId/users/:userId', { orgId: 'a', userId: 'b' });
|
||||||
|
* // => '/orgs/a/users/b'
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function interpolateUrl(
|
||||||
|
template: string,
|
||||||
|
variables?: Record<string, string>,
|
||||||
|
): string {
|
||||||
|
if (!variables || Object.keys(variables).length === 0) {
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
|
||||||
|
return template
|
||||||
|
.split('/')
|
||||||
|
.map((segment) => {
|
||||||
|
if (segment.startsWith(':')) {
|
||||||
|
const key = segment.slice(1);
|
||||||
|
const value = variables[key];
|
||||||
|
if (value === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`[interpolateUrl] Missing value for URL parameter ":${key}" in template "${template}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return encodeURIComponent(value);
|
||||||
|
}
|
||||||
|
return segment;
|
||||||
|
})
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { AxiosError, type AxiosResponse } from 'axios';
|
||||||
|
import { ApiErrorCode, httpStatusToErrorCode } from './error-codes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structured API error that normalizes Axios errors into a
|
||||||
|
* predictable, serializable format.
|
||||||
|
*
|
||||||
|
* Replaces the legacy `ErrorRequest` class with richer metadata.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* try {
|
||||||
|
* await apiClient.get('/users');
|
||||||
|
* } catch (err) {
|
||||||
|
* if (err instanceof ApiError) {
|
||||||
|
* console.log(err.code); // ApiErrorCode.UNAUTHORIZED
|
||||||
|
* console.log(err.status); // 401
|
||||||
|
* console.log(err.data); // { message: "Token expired" }
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class ApiError extends Error {
|
||||||
|
/** Structured error code for programmatic handling. */
|
||||||
|
readonly code: ApiErrorCode;
|
||||||
|
|
||||||
|
/** HTTP status code (0 if no response, e.g., network error). */
|
||||||
|
readonly status: number;
|
||||||
|
|
||||||
|
/** Raw response body from the server, if available. */
|
||||||
|
readonly data: unknown;
|
||||||
|
|
||||||
|
/** The original Axios error, preserved for debugging. */
|
||||||
|
readonly cause: AxiosError | undefined;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
code: ApiErrorCode,
|
||||||
|
status: number,
|
||||||
|
data?: unknown,
|
||||||
|
cause?: AxiosError,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.code = code;
|
||||||
|
this.status = status;
|
||||||
|
this.data = data;
|
||||||
|
this.cause = cause;
|
||||||
|
|
||||||
|
// Maintain proper prototype chain for instanceof checks
|
||||||
|
Object.setPrototypeOf(this, ApiError.prototype);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory: creates an ApiError from an AxiosError.
|
||||||
|
* Automatically resolves the error code from the HTTP status.
|
||||||
|
*/
|
||||||
|
static fromAxiosError(error: AxiosError<unknown>): ApiError {
|
||||||
|
// Network error (no response received)
|
||||||
|
if (!error.response) {
|
||||||
|
if (error.code === 'ECONNABORTED') {
|
||||||
|
return new ApiError(
|
||||||
|
'Request timed out',
|
||||||
|
ApiErrorCode.TIMEOUT,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (error.code === 'ERR_CANCELED') {
|
||||||
|
return new ApiError(
|
||||||
|
'Request was cancelled',
|
||||||
|
ApiErrorCode.CANCELLED,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return new ApiError(
|
||||||
|
error.message || 'Network error',
|
||||||
|
ApiErrorCode.NETWORK_ERROR,
|
||||||
|
0,
|
||||||
|
undefined,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server responded with an error status
|
||||||
|
const response: AxiosResponse = error.response;
|
||||||
|
const status = response.status;
|
||||||
|
const data = response.data;
|
||||||
|
const code = httpStatusToErrorCode(status);
|
||||||
|
|
||||||
|
// Extract message from common server response formats
|
||||||
|
const serverMessage =
|
||||||
|
(data && typeof data === 'object' && 'message' in data)
|
||||||
|
? String((data as Record<string, unknown>).message)
|
||||||
|
: `Request failed with status ${status}`;
|
||||||
|
|
||||||
|
return new ApiError(serverMessage, code, status, data, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience check for authentication failures. */
|
||||||
|
get isUnauthorized(): boolean {
|
||||||
|
return this.code === ApiErrorCode.UNAUTHORIZED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience check for permission failures. */
|
||||||
|
get isForbidden(): boolean {
|
||||||
|
return this.code === ApiErrorCode.FORBIDDEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Convenience check for network/connectivity issues. */
|
||||||
|
get isNetworkError(): boolean {
|
||||||
|
return this.code === ApiErrorCode.NETWORK_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** JSON-serializable representation for logging/telemetry. */
|
||||||
|
toJSON(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
name: this.name,
|
||||||
|
message: this.message,
|
||||||
|
code: this.code,
|
||||||
|
status: this.status,
|
||||||
|
data: this.data,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Enumeration of well-known API error codes.
|
||||||
|
*
|
||||||
|
* Use these to programmatically handle specific server responses
|
||||||
|
* without relying on magic strings scattered across the codebase.
|
||||||
|
*/
|
||||||
|
export enum ApiErrorCode {
|
||||||
|
// ─── HTTP Standard ────────────────────────────────────────────
|
||||||
|
BAD_REQUEST = 'BAD_REQUEST',
|
||||||
|
UNAUTHORIZED = 'UNAUTHORIZED',
|
||||||
|
FORBIDDEN = 'FORBIDDEN',
|
||||||
|
NOT_FOUND = 'NOT_FOUND',
|
||||||
|
CONFLICT = 'CONFLICT',
|
||||||
|
UNPROCESSABLE_ENTITY = 'UNPROCESSABLE_ENTITY',
|
||||||
|
TOO_MANY_REQUESTS = 'TOO_MANY_REQUESTS',
|
||||||
|
INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR',
|
||||||
|
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
|
||||||
|
|
||||||
|
// ─── Client-side ──────────────────────────────────────────────
|
||||||
|
NETWORK_ERROR = 'NETWORK_ERROR',
|
||||||
|
TIMEOUT = 'TIMEOUT',
|
||||||
|
CANCELLED = 'CANCELLED',
|
||||||
|
UNKNOWN = 'UNKNOWN',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps HTTP status codes to ApiErrorCode enum values.
|
||||||
|
*/
|
||||||
|
export function httpStatusToErrorCode(status: number): ApiErrorCode {
|
||||||
|
switch (status) {
|
||||||
|
case 400: return ApiErrorCode.BAD_REQUEST;
|
||||||
|
case 401: return ApiErrorCode.UNAUTHORIZED;
|
||||||
|
case 403: return ApiErrorCode.FORBIDDEN;
|
||||||
|
case 404: return ApiErrorCode.NOT_FOUND;
|
||||||
|
case 409: return ApiErrorCode.CONFLICT;
|
||||||
|
case 422: return ApiErrorCode.UNPROCESSABLE_ENTITY;
|
||||||
|
case 429: return ApiErrorCode.TOO_MANY_REQUESTS;
|
||||||
|
case 500: return ApiErrorCode.INTERNAL_SERVER_ERROR;
|
||||||
|
case 503: return ApiErrorCode.SERVICE_UNAVAILABLE;
|
||||||
|
default: return ApiErrorCode.UNKNOWN;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { ApiError } from './api-error';
|
||||||
|
export { ApiErrorCode, httpStatusToErrorCode } from './error-codes';
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
import axios, { type AxiosInstance, type AxiosError } from 'axios';
|
||||||
|
import type { HttpClientConfig, InterceptorHooks } from './types';
|
||||||
|
import { noopObservabilityAdapter } from '../observability/noop.adapter';
|
||||||
|
import { ApiError } from '../errors/api-error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an isolated Axios instance with per-app configuration.
|
||||||
|
*
|
||||||
|
* **CRITICAL**: This function creates a NEW AxiosInstance on every call.
|
||||||
|
* It NEVER touches `axios.defaults` or `axios.interceptors`. Each consumer
|
||||||
|
* receives a fully autonomous client with its own interceptor chain.
|
||||||
|
*
|
||||||
|
* @param config - Base configuration (URL, timeout, headers, observability).
|
||||||
|
* @param hooks - Optional per-app interceptor hooks for auth, error handling, etc.
|
||||||
|
* @returns A configured, isolated AxiosInstance.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // apps/web — full auth + telemetry
|
||||||
|
* const apiClient = createHttpClient(
|
||||||
|
* { baseURL: 'https://api.eigen.co/v1', observability: otelAdapter },
|
||||||
|
* {
|
||||||
|
* onRequest: async (config) => {
|
||||||
|
* config.headers.Authorization = `Bearer ${getToken()}`;
|
||||||
|
* return config;
|
||||||
|
* },
|
||||||
|
* onResponseError: async (error) => {
|
||||||
|
* if (error.response?.status === 401) redirect('/login');
|
||||||
|
* throw error;
|
||||||
|
* },
|
||||||
|
* },
|
||||||
|
* );
|
||||||
|
*
|
||||||
|
* // apps/landing — minimal public client
|
||||||
|
* const publicClient = createHttpClient({
|
||||||
|
* baseURL: 'https://api.eigen.co/public/v1',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function createHttpClient(
|
||||||
|
config: HttpClientConfig,
|
||||||
|
hooks?: InterceptorHooks,
|
||||||
|
): AxiosInstance {
|
||||||
|
const observability = config.observability ?? noopObservabilityAdapter;
|
||||||
|
|
||||||
|
// ── Create isolated instance ──────────────────────────────────
|
||||||
|
const instance = axios.create({
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
timeout: config.timeout ?? 15000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
...(config.defaultHeaders ?? {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Request Interceptor Chain ─────────────────────────────────
|
||||||
|
instance.interceptors.request.use(
|
||||||
|
async (reqConfig) => {
|
||||||
|
// 1. Observability hook (tracing span start)
|
||||||
|
// Wrapped in try-catch: adapter failures must never block the request
|
||||||
|
try {
|
||||||
|
observability.onRequestStart(reqConfig);
|
||||||
|
} catch (adapterError) {
|
||||||
|
console.warn('[core-api] Observability adapter error in onRequestStart:', adapterError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. App-specific hook (e.g., inject auth token)
|
||||||
|
if (hooks?.onRequest) {
|
||||||
|
return hooks.onRequest(reqConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqConfig;
|
||||||
|
},
|
||||||
|
(error) => Promise.reject(error),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Response Interceptor Chain ────────────────────────────────
|
||||||
|
instance.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// 1. Observability hook (tracing span end)
|
||||||
|
try {
|
||||||
|
observability.onRequestEnd(response);
|
||||||
|
} catch (adapterError) {
|
||||||
|
console.warn('[core-api] Observability adapter error in onRequestEnd:', adapterError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. App-specific response transform
|
||||||
|
if (hooks?.onResponse) {
|
||||||
|
return hooks.onResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
async (error: AxiosError) => {
|
||||||
|
// 1. Observability hook (tracing error record)
|
||||||
|
// CRITICAL: Wrapped in try-catch so adapter crashes never
|
||||||
|
// swallow the original API error from the UI.
|
||||||
|
try {
|
||||||
|
observability.onRequestError(error);
|
||||||
|
} catch (adapterError) {
|
||||||
|
console.warn('[core-api] Observability adapter error in onRequestError:', adapterError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. App-specific error handler (e.g., 401 redirect)
|
||||||
|
if (hooks?.onResponseError) {
|
||||||
|
return hooks.onResponseError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Default: wrap in structured ApiError
|
||||||
|
throw ApiError.fromAxiosError(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export { createHttpClient } from './create-http-client';
|
||||||
|
export type {
|
||||||
|
HttpClientConfig,
|
||||||
|
InterceptorHooks,
|
||||||
|
ApiResponse,
|
||||||
|
TelemetryContext,
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosError,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from './types';
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import type {
|
||||||
|
AxiosError,
|
||||||
|
AxiosResponse,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
|
import type { IObservabilityAdapter } from '../observability/types';
|
||||||
|
|
||||||
|
// ─── Factory Configuration ──────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for creating an isolated HTTP client instance.
|
||||||
|
* Each app provides its own config — no globals are shared.
|
||||||
|
*/
|
||||||
|
export interface HttpClientConfig {
|
||||||
|
/** Base URL for all requests (e.g., 'https://api.eigen.co/v1'). */
|
||||||
|
baseURL: string;
|
||||||
|
|
||||||
|
/** Default request timeout in milliseconds. @default 15000 */
|
||||||
|
timeout?: number;
|
||||||
|
|
||||||
|
/** Default headers applied to every outgoing request. */
|
||||||
|
defaultHeaders?: Record<string, string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observability adapter for tracing, logging, and metrics.
|
||||||
|
* If not provided, a zero-overhead No-Op adapter is used.
|
||||||
|
*/
|
||||||
|
observability?: IObservabilityAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Interceptor Hooks ──────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-app hooks for customizing request/response behavior.
|
||||||
|
*
|
||||||
|
* These hooks are the app's "autonomy layer" — each app decides
|
||||||
|
* how to inject tokens, handle 401s, transform responses, etc.
|
||||||
|
*/
|
||||||
|
export interface InterceptorHooks {
|
||||||
|
/**
|
||||||
|
* Called before every request is dispatched.
|
||||||
|
* Use this to inject authentication tokens, tenant headers, etc.
|
||||||
|
*/
|
||||||
|
onRequest?: (
|
||||||
|
config: InternalAxiosRequestConfig,
|
||||||
|
) => Promise<InternalAxiosRequestConfig> | InternalAxiosRequestConfig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on every successful response (2xx status).
|
||||||
|
* Use this to normalize response shapes if needed.
|
||||||
|
*/
|
||||||
|
onResponse?: (response: AxiosResponse) => AxiosResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called on every failed response (non-2xx or network error).
|
||||||
|
* Use this for app-specific error handling (e.g., redirect on 401).
|
||||||
|
* MUST throw or return a rejected promise.
|
||||||
|
*/
|
||||||
|
onResponseError?: (error: AxiosError) => Promise<never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Standardized API Response ──────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type-safe API response wrapper.
|
||||||
|
*
|
||||||
|
* Replaces the legacy `ResponseEntity` and the lost-in-callback
|
||||||
|
* `Promise<void>` return type with a fully typed contract.
|
||||||
|
*/
|
||||||
|
export interface ApiResponse<T = unknown> {
|
||||||
|
/** The parsed response body. */
|
||||||
|
data: T;
|
||||||
|
|
||||||
|
/** The HTTP status code. */
|
||||||
|
status: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Per-Request Telemetry Context ──────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced escape hatch for per-request telemetry enrichment.
|
||||||
|
*
|
||||||
|
* Attach this to any request to push custom spans, tags, or
|
||||||
|
* business events into the observability pipeline.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* await bookingServices.getMany({
|
||||||
|
* telemetryContext: {
|
||||||
|
* customSpanName: 'booking.list.fetch',
|
||||||
|
* tags: { region: 'asia', priority: 'high' },
|
||||||
|
* pushEventOnSuccess: 'booking_list_loaded',
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export interface TelemetryContext {
|
||||||
|
/** Custom keys/tags to enrich the Faro log/error or OTel Span. */
|
||||||
|
tags?: Record<string, string | number | boolean>;
|
||||||
|
/** If provided, manually starts a custom OTel span wrapping this request. */
|
||||||
|
customSpanName?: string;
|
||||||
|
/** Force an explicit business event to be pushed to Faro on success. */
|
||||||
|
pushEventOnSuccess?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Re-export Axios types consumers frequently need ────────────
|
||||||
|
|
||||||
|
export type {
|
||||||
|
AxiosInstance,
|
||||||
|
AxiosError,
|
||||||
|
AxiosResponse,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
InternalAxiosRequestConfig,
|
||||||
|
} from 'axios';
|
||||||
|
|
||||||
|
// ─── Augment Axios to carry TelemetryContext ────────────────────
|
||||||
|
|
||||||
|
declare module 'axios' {
|
||||||
|
interface AxiosRequestConfig {
|
||||||
|
/** Per-request telemetry context for custom spans, tags, events. */
|
||||||
|
telemetryContext?: TelemetryContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export type { IObservabilityAdapter } from './types';
|
||||||
|
export { noopObservabilityAdapter } from './noop.adapter';
|
||||||
|
export { faroAdapter } from './otel.adapter';
|
||||||
|
export { initTelemetry, getFaro } from './setup';
|
||||||
|
export type { TelemetryConfig } from './setup';
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { IObservabilityAdapter } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-Op Observability Adapter.
|
||||||
|
*
|
||||||
|
* Default adapter when no telemetry is configured.
|
||||||
|
* All methods are empty — V8's TurboFan JIT compiler will inline
|
||||||
|
* and dead-code-eliminate these calls during optimization,
|
||||||
|
* resulting in effectively ZERO runtime overhead.
|
||||||
|
*
|
||||||
|
* Used by apps that don't need APM (e.g., `apps/landing`).
|
||||||
|
*/
|
||||||
|
export const noopObservabilityAdapter: IObservabilityAdapter = {
|
||||||
|
onRequestStart() {},
|
||||||
|
onRequestEnd() {},
|
||||||
|
onRequestError() {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { trace, SpanStatusCode, type Span } from '@opentelemetry/api';
|
||||||
|
import { LogLevel } from '@grafana/faro-web-sdk';
|
||||||
|
import type { IObservabilityAdapter } from './types';
|
||||||
|
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||||
|
import type { TelemetryContext } from '../http-client/types';
|
||||||
|
import { getFaro } from './setup';
|
||||||
|
|
||||||
|
// ─── Symbol Keys ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Symbol-keyed storage for custom spans on the Axios config. */
|
||||||
|
const CUSTOM_SPAN_KEY = Symbol('__customOtelSpan');
|
||||||
|
|
||||||
|
function attachSpan(config: InternalAxiosRequestConfig, span: Span): void {
|
||||||
|
(config as unknown as Record<symbol, Span>)[CUSTOM_SPAN_KEY] = span;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpan(config: unknown): Span | undefined {
|
||||||
|
if (!config) return undefined;
|
||||||
|
return (config as Record<symbol, Span>)?.[CUSTOM_SPAN_KEY];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely close a span, guarding against double-close.
|
||||||
|
* After ending, removes the reference from the config to prevent
|
||||||
|
* duplicate Span ID errors on potential Axios retries.
|
||||||
|
*/
|
||||||
|
function safeEndSpan(config: unknown, span: Span): void {
|
||||||
|
span.end();
|
||||||
|
// Detach from config to prevent double-close on retry
|
||||||
|
if (config) {
|
||||||
|
delete (config as Record<symbol, unknown>)[CUSTOM_SPAN_KEY];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract TelemetryContext from an Axios config. */
|
||||||
|
function getTelemetryContext(config: unknown): TelemetryContext | undefined {
|
||||||
|
if (!config) return undefined;
|
||||||
|
return (config as Record<string, unknown>)?.telemetryContext as TelemetryContext | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Convert TelemetryContext tags to a string record for Faro context. */
|
||||||
|
function tagsToFaroContext(tags?: Record<string, string | number | boolean>): Record<string, string> {
|
||||||
|
if (!tags) return {};
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(tags).map(([k, v]) => [k, String(v)]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the standardized base context used by ALL Faro pushLog/pushError calls.
|
||||||
|
* Ensures `module.key`, `module.action`, and tags are always at the top-level
|
||||||
|
* `context` object — making them directly queryable in LogQL (Loki).
|
||||||
|
*/
|
||||||
|
function buildBaseContext(
|
||||||
|
method: string,
|
||||||
|
url: string,
|
||||||
|
moduleKey?: string,
|
||||||
|
action?: string,
|
||||||
|
tags?: Record<string, string | number | boolean>,
|
||||||
|
): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'http.method': method,
|
||||||
|
'http.url': url,
|
||||||
|
...(moduleKey ? { 'module.key': moduleKey } : {}),
|
||||||
|
...(action ? { 'module.action': action } : {}),
|
||||||
|
...tagsToFaroContext(tags),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Adapter ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Production-grade Observability Adapter.
|
||||||
|
*
|
||||||
|
* Strategy: **Opt-In Custom Spans + Faro/Loki Baseline Logging**
|
||||||
|
*
|
||||||
|
* `trace.getActiveSpan()` returns `undefined` inside Axios interceptors
|
||||||
|
* due to browser XHR/Fetch lifecycle race conditions with Faro's
|
||||||
|
* `TracingInstrumentation`. Therefore this adapter does NOT attempt
|
||||||
|
* to enrich auto-instrumented spans.
|
||||||
|
*
|
||||||
|
* Instead it focuses on two responsibilities:
|
||||||
|
*
|
||||||
|
* 1. **Custom Span Mode** (opt-in via `telemetryContext.customSpanName`):
|
||||||
|
* Creates an explicit OTel span, attaches business tags, and ensures
|
||||||
|
* the span is ALWAYS closed — even on abort, timeout, or unexpected
|
||||||
|
* errors — to prevent span leaks.
|
||||||
|
*
|
||||||
|
* 2. **Faro/Loki Baseline** (always):
|
||||||
|
* Pushes rich contextual logs (`pushLog`), errors (`pushError`), and
|
||||||
|
* success events (`pushEvent`) with standardized `baseContext` for
|
||||||
|
* direct LogQL queryability.
|
||||||
|
*
|
||||||
|
* Safety guarantees:
|
||||||
|
* - Spans are always closed via `safeEndSpan()` which detaches the
|
||||||
|
* reference after closing, preventing double-close on Axios retries.
|
||||||
|
* - Adapter errors are caught internally and never swallowed — the
|
||||||
|
* original API error always propagates to the UI.
|
||||||
|
* - `null`/`undefined` config guards prevent crashes on network timeouts
|
||||||
|
* where `error.config` may be undefined.
|
||||||
|
*/
|
||||||
|
export const faroAdapter: IObservabilityAdapter = {
|
||||||
|
onRequestStart(config: InternalAxiosRequestConfig) {
|
||||||
|
const ctx = getTelemetryContext(config);
|
||||||
|
const method = (config.method ?? 'UNKNOWN').toUpperCase();
|
||||||
|
const url = config.url ?? '/';
|
||||||
|
const moduleKey = config.headers?.['ex-module-key'] as string | undefined;
|
||||||
|
const action = config.headers?.['ex-module-action'] as string | undefined;
|
||||||
|
|
||||||
|
// ── 1. Create optional custom span ──────────────────────────
|
||||||
|
if (ctx?.customSpanName) {
|
||||||
|
const tracer = trace.getTracer('@repo/core-api', '1.0.0');
|
||||||
|
const span = tracer.startSpan(ctx.customSpanName, {
|
||||||
|
attributes: {
|
||||||
|
'http.method': method,
|
||||||
|
'http.url': url,
|
||||||
|
...(moduleKey ? { 'custom.module_key': moduleKey } : {}),
|
||||||
|
...(action ? { 'custom.module_action': action } : {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach custom tags with `custom.` prefix
|
||||||
|
if (ctx.tags) {
|
||||||
|
for (const [key, value] of Object.entries(ctx.tags)) {
|
||||||
|
span.setAttribute(`custom.${key}`, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attachSpan(config, span);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Push structured log (Loki) ───────────────────────────
|
||||||
|
const faro = getFaro();
|
||||||
|
if (faro) {
|
||||||
|
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
|
||||||
|
faro.api.pushLog(
|
||||||
|
[`[core-api] ${method} ${url}`],
|
||||||
|
{ level: LogLevel.DEBUG, context: baseContext },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequestEnd(response: AxiosResponse) {
|
||||||
|
const ctx = getTelemetryContext(response.config);
|
||||||
|
const moduleKey = response.config?.headers?.['ex-module-key'] as string | undefined;
|
||||||
|
const action = response.config?.headers?.['ex-module-action'] as string | undefined;
|
||||||
|
|
||||||
|
// ── 1. Close custom span (OK) ───────────────────────────────
|
||||||
|
const customSpan = getSpan(response.config);
|
||||||
|
if (customSpan) {
|
||||||
|
customSpan.setAttribute('http.status_code', response.status);
|
||||||
|
customSpan.setStatus({ code: SpanStatusCode.OK });
|
||||||
|
safeEndSpan(response.config, customSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Push success event (Faro) ────────────────────────────
|
||||||
|
if (ctx?.pushEventOnSuccess) {
|
||||||
|
const faro = getFaro();
|
||||||
|
if (faro) {
|
||||||
|
const method = (response.config?.method ?? 'UNKNOWN').toUpperCase();
|
||||||
|
const url = response.config?.url ?? '/';
|
||||||
|
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx.tags);
|
||||||
|
faro.api.pushEvent(ctx.pushEventOnSuccess, {
|
||||||
|
...baseContext,
|
||||||
|
'http.status_code': String(response.status),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onRequestError(error: AxiosError) {
|
||||||
|
const ctx = getTelemetryContext(error.config);
|
||||||
|
const status = error.response?.status ?? 0;
|
||||||
|
const method = (error.config?.method ?? 'UNKNOWN').toUpperCase();
|
||||||
|
const url = error.config?.url ?? '/';
|
||||||
|
const moduleKey = error.config?.headers?.['ex-module-key'] as string | undefined;
|
||||||
|
const action = error.config?.headers?.['ex-module-action'] as string | undefined;
|
||||||
|
|
||||||
|
// Standardized context for ALL Faro calls in this handler
|
||||||
|
const baseContext = buildBaseContext(method, url, moduleKey, action, ctx?.tags);
|
||||||
|
const errorContext: Record<string, string> = {
|
||||||
|
...baseContext,
|
||||||
|
'http.status_code': String(status),
|
||||||
|
'error.message': error.message,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── 1. Close custom span with error (if present) ────────────
|
||||||
|
const customSpan = getSpan(error.config);
|
||||||
|
if (customSpan) {
|
||||||
|
customSpan.setAttribute('http.status_code', status);
|
||||||
|
customSpan.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
|
||||||
|
customSpan.recordException(error);
|
||||||
|
safeEndSpan(error.config, customSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 2. Push structured error + log (Faro → Loki) ────────────
|
||||||
|
const faro = getFaro();
|
||||||
|
if (faro) {
|
||||||
|
faro.api.pushError(error, {
|
||||||
|
type: 'api_error',
|
||||||
|
context: errorContext,
|
||||||
|
});
|
||||||
|
|
||||||
|
faro.api.pushLog(
|
||||||
|
[`[core-api] ERROR ${method} ${url} → ${status}`],
|
||||||
|
{ level: LogLevel.ERROR, context: errorContext },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
/**
|
||||||
|
* Centralized Telemetry Setup — Grafana Faro + OpenTelemetry.
|
||||||
|
*
|
||||||
|
* Provides a plug-and-play `initTelemetry()` function that hides
|
||||||
|
* all Faro/OTel complexity behind a simple config interface.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // apps/web/src/main.tsx (top of file)
|
||||||
|
* import { initTelemetry } from '@repo/core-api/observability/setup';
|
||||||
|
* initTelemetry({
|
||||||
|
* appName: 'web',
|
||||||
|
* appVersion: '1.0.0',
|
||||||
|
* telemetryUrl: 'https://telemetry.eigen.co.id/collect',
|
||||||
|
* environment: 'production',
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
getWebInstrumentations,
|
||||||
|
initializeFaro,
|
||||||
|
type Faro,
|
||||||
|
} from '@grafana/faro-react';
|
||||||
|
import { TracingInstrumentation } from '@grafana/faro-web-tracing';
|
||||||
|
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
|
||||||
|
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-web';
|
||||||
|
|
||||||
|
// ─── Configuration Interface ────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration for initializing the full telemetry stack.
|
||||||
|
* Apps pass this once at startup — everything else is automatic.
|
||||||
|
*/
|
||||||
|
export interface TelemetryConfig {
|
||||||
|
/** Application name for Faro + OTel resource attributes. */
|
||||||
|
appName: string;
|
||||||
|
/** Application version (SemVer). */
|
||||||
|
appVersion: string;
|
||||||
|
/** Grafana Faro collector URL (e.g., 'https://telemetry.eigen.co.id/collect'). */
|
||||||
|
telemetryUrl: string;
|
||||||
|
/** Deployment environment ('production', 'staging', 'development'). */
|
||||||
|
environment: string;
|
||||||
|
/**
|
||||||
|
* Optional: Separate OTLP trace endpoint for direct Tempo ingestion.
|
||||||
|
* If not provided, traces are only sent through Faro's built-in exporter.
|
||||||
|
*/
|
||||||
|
otlpTraceUrl?: string;
|
||||||
|
/**
|
||||||
|
* Optional: CORS URL patterns for W3C trace context propagation.
|
||||||
|
* @default [/.* /]
|
||||||
|
*/
|
||||||
|
propagateTraceHeaderCorsUrls?: Array<string | RegExp>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Session Persistence ────────────────────────────────────────
|
||||||
|
|
||||||
|
const FARO_SESSION_KEY = 'faroSession';
|
||||||
|
|
||||||
|
function getStoredSession(): Record<string, unknown> | null {
|
||||||
|
try {
|
||||||
|
const data = localStorage.getItem(FARO_SESSION_KEY);
|
||||||
|
return data ? JSON.parse(data) : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Singleton ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let faroInstance: Faro | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the Grafana Faro + OpenTelemetry observability stack.
|
||||||
|
*
|
||||||
|
* Call this ONCE at the top of your app's entry point, before any
|
||||||
|
* React code, HTTP requests, or other imports execute.
|
||||||
|
*
|
||||||
|
* `TracingInstrumentation` internally handles:
|
||||||
|
* - WebTracerProvider setup with resource attributes
|
||||||
|
* - FaroMetaAttributesSpanProcessor (session/user enrichment)
|
||||||
|
* - FaroTraceExporter (sends spans to the Faro collector)
|
||||||
|
* - Auto-instrumentation for fetch/XHR
|
||||||
|
* - `faro.api.initOTEL(trace, context)` bridge
|
||||||
|
*
|
||||||
|
* @returns The initialized Faro instance for advanced usage.
|
||||||
|
*/
|
||||||
|
export function initTelemetry(config: TelemetryConfig): Faro {
|
||||||
|
if (faroInstance) return faroInstance;
|
||||||
|
|
||||||
|
const storedSession = getStoredSession();
|
||||||
|
|
||||||
|
// Build optional extra span processors
|
||||||
|
const tracingOptions: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (config.otlpTraceUrl) {
|
||||||
|
tracingOptions.spanProcessor = new BatchSpanProcessor(
|
||||||
|
new OTLPTraceExporter({
|
||||||
|
url: config.otlpTraceUrl,
|
||||||
|
headers: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
faroInstance = initializeFaro({
|
||||||
|
url: config.telemetryUrl,
|
||||||
|
app: {
|
||||||
|
name: config.appName,
|
||||||
|
version: config.appVersion,
|
||||||
|
environment: config.environment,
|
||||||
|
},
|
||||||
|
sessionTracking: {
|
||||||
|
enabled: true,
|
||||||
|
persistent: true,
|
||||||
|
session: storedSession ?? undefined,
|
||||||
|
onSessionChange: (_oldSession, newSession) => {
|
||||||
|
if (newSession) {
|
||||||
|
localStorage.setItem(FARO_SESSION_KEY, JSON.stringify(newSession));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
instrumentations: [
|
||||||
|
...getWebInstrumentations(),
|
||||||
|
|
||||||
|
new TracingInstrumentation({
|
||||||
|
...tracingOptions,
|
||||||
|
instrumentationOptions: {
|
||||||
|
propagateTraceHeaderCorsUrls:
|
||||||
|
config.propagateTraceHeaderCorsUrls ?? [/.*/],
|
||||||
|
fetchInstrumentationOptions: {
|
||||||
|
applyCustomAttributesOnSpan(span) {
|
||||||
|
span.setAttribute('app.synthetic_request', 'false');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xhrInstrumentationOptions: {
|
||||||
|
applyCustomAttributesOnSpan(span) {
|
||||||
|
span.setAttribute('app.synthetic_request', 'false');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return faroInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the Faro instance (null if not yet initialized). */
|
||||||
|
export function getFaro(): Faro | null {
|
||||||
|
return faroInstance;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface-driven observability contract.
|
||||||
|
*
|
||||||
|
* The HTTP client calls these hooks at request lifecycle points.
|
||||||
|
* Implementations decide whether to trace, log, metric, or do nothing.
|
||||||
|
*
|
||||||
|
* This interface is the ONLY dependency between the HTTP client and
|
||||||
|
* any telemetry library. The core-api package NEVER imports
|
||||||
|
* OpenTelemetry, Datadog, Sentry, or any vendor SDK directly.
|
||||||
|
*/
|
||||||
|
export interface IObservabilityAdapter {
|
||||||
|
/** Called immediately before a request is dispatched. */
|
||||||
|
onRequestStart(config: InternalAxiosRequestConfig): void;
|
||||||
|
|
||||||
|
/** Called when a response is successfully received. */
|
||||||
|
onRequestEnd(response: AxiosResponse): void;
|
||||||
|
|
||||||
|
/** Called when a request fails (network error or error status). */
|
||||||
|
onRequestError(error: AxiosError): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/typescript-config/library.json",
|
||||||
|
"include": ["src"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"strict": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+534
-61
@@ -96,10 +96,13 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.4.17
|
version: 5.4.17(@types/node@22.19.3)
|
||||||
|
|
||||||
apps/landing:
|
apps/landing:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@repo/core-api':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/core-api
|
||||||
'@repo/ui':
|
'@repo/ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/ui
|
version: link:../../packages/ui
|
||||||
@@ -142,10 +145,13 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.4.17
|
version: 5.4.17(@types/node@22.19.3)
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@repo/core-api':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/core-api
|
||||||
'@repo/ui':
|
'@repo/ui':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../../packages/ui
|
version: link:../../packages/ui
|
||||||
@@ -194,10 +200,10 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.4.17
|
version: 5.4.17(@types/node@22.19.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17
|
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
packages/configs/eslint:
|
packages/configs/eslint:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -236,6 +242,46 @@ importers:
|
|||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.1
|
version: 8.57.1
|
||||||
|
|
||||||
|
packages/core-api:
|
||||||
|
dependencies:
|
||||||
|
'@grafana/faro-react':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.7.0(react@19.2.3)
|
||||||
|
'@grafana/faro-web-sdk':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.7.0
|
||||||
|
'@grafana/faro-web-tracing':
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.7.0
|
||||||
|
'@opentelemetry/api':
|
||||||
|
specifier: ^1.9.0
|
||||||
|
version: 1.9.1
|
||||||
|
'@opentelemetry/exporter-trace-otlp-http':
|
||||||
|
specifier: ^0.213.0
|
||||||
|
version: 0.213.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-web':
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
axios:
|
||||||
|
specifier: ^1.9.0
|
||||||
|
version: 1.16.1
|
||||||
|
devDependencies:
|
||||||
|
'@repo/eslint-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../configs/eslint
|
||||||
|
'@repo/typescript-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../configs/typescript
|
||||||
|
eslint:
|
||||||
|
specifier: ^8.57.1
|
||||||
|
version: 8.57.1
|
||||||
|
typescript:
|
||||||
|
specifier: 5.5.4
|
||||||
|
version: 5.5.4
|
||||||
|
vitest:
|
||||||
|
specifier: ^4.0.17
|
||||||
|
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mantine/core':
|
'@mantine/core':
|
||||||
@@ -292,10 +338,10 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vite:
|
vite:
|
||||||
specifier: ^5.1.4
|
specifier: ^5.1.4
|
||||||
version: 5.4.17
|
version: 5.4.17(@types/node@22.19.3)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17
|
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
packages/utils:
|
packages/utils:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -323,7 +369,7 @@ importers:
|
|||||||
version: 5.5.4
|
version: 5.5.4
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.17
|
specifier: ^4.0.17
|
||||||
version: 4.0.17
|
version: 4.0.17(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1156,6 +1202,62 @@ packages:
|
|||||||
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
|
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@grafana/faro-core@2.7.0:
|
||||||
|
resolution: {integrity: sha512-fNOfWEouoHhZ4dw+oLO+UGoMBLgiLLQU56dp9+NgO3mGNwCYsFTQuKgKQfOe5DDmAbwpRISEyLbiMkEyY7mIJQ==}
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@grafana/faro-react@2.7.0(react@19.2.3):
|
||||||
|
resolution: {integrity: sha512-95wmypcDjHdGe64TCivq3DKLut9q3RRANgKoSLSQhGHmXdKTwE2Ov0+UdUkVQ9uu3KsGGQJRuXkldP0YMIEMiw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
react-router: ^7.12.0
|
||||||
|
react-router-dom: ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
react-router:
|
||||||
|
optional: true
|
||||||
|
react-router-dom:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@grafana/faro-web-sdk': 2.7.0
|
||||||
|
'@grafana/faro-web-tracing': 2.7.0
|
||||||
|
hoist-non-react-statics: 3.3.2
|
||||||
|
react: 19.2.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@grafana/faro-web-sdk@2.7.0:
|
||||||
|
resolution: {integrity: sha512-NIbFK2g634/KOcq6ej1LseZrbbRejBiIrO8beQpYX6CkyXVGrcdoWRfJdqkQ3Ys60h4aTH+xoAh2Oh0HebhOZg==}
|
||||||
|
dependencies:
|
||||||
|
'@grafana/faro-core': 2.7.0
|
||||||
|
ua-parser-js: 1.0.41
|
||||||
|
web-vitals: 5.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@grafana/faro-web-tracing@2.7.0:
|
||||||
|
resolution: {integrity: sha512-k6eIJeLwm60FpCB+OevL+VLqnoibtCsnozs3ZCQpiUMt6G2PUIQ6/wkHfSEF46R2H+AuVB3nk9oHUKpS5o2wQQ==}
|
||||||
|
dependencies:
|
||||||
|
'@grafana/faro-web-sdk': 2.7.0
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/exporter-trace-otlp-http': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/instrumentation-fetch': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/instrumentation-xml-http-request': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@humanwhocodes/config-array@0.13.0:
|
/@humanwhocodes/config-array@0.13.0:
|
||||||
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==}
|
||||||
engines: {node: '>=10.10.0'}
|
engines: {node: '>=10.10.0'}
|
||||||
@@ -1197,7 +1299,7 @@ packages:
|
|||||||
magic-string: 0.27.0
|
magic-string: 0.27.0
|
||||||
react-docgen-typescript: 2.4.0(typescript@5.5.4)
|
react-docgen-typescript: 2.4.0(typescript@5.5.4)
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
vite: 5.4.17
|
vite: 5.4.17(@types/node@22.19.3)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@jridgewell/gen-mapping@0.3.13:
|
/@jridgewell/gen-mapping@0.3.13:
|
||||||
@@ -1443,6 +1545,279 @@ packages:
|
|||||||
which: 4.0.0
|
which: 4.0.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/api-logs@0.213.0:
|
||||||
|
resolution: {integrity: sha512-zRM5/Qj6G84Ej3F1yt33xBVY/3tnMxtL1fiDIxYbDWYaZ/eudVw3/PBiZ8G7JwUxXxjW8gU4g6LnOyfGKYHYgw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/api-logs@0.218.0:
|
||||||
|
resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/api@1.9.1:
|
||||||
|
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
|
/@opentelemetry/core@2.6.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/exporter-trace-otlp-http@0.213.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-tnRmJD39aWrE/Sp7F6AbRNAjKHToDkAqBi6i0lESpGWz3G+f4bhVAV6mgSXH2o18lrDVJXo6jf9bAywQw43wRA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-exporter-base': 0.213.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/exporter-trace-otlp-http@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-8dqezsmPhtKitIK/eTipZhYl9EX2/gNQ5zUMhaz3uxEURwfkNf8IPvo6yNfrzbxdtpAOybS/+h7wmIWYqFSpiw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-exporter-base': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/instrumentation-fetch@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-eP/Y5hDupb+6MwZSaMw4ZdsDz8YgfJbJ4Ta86BMVeOmI2EArwXcd0v1nfNIvUzXTPi7nakidwqeuUa3FwRwECg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/instrumentation-xml-http-request@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-KzqSO62lTiqbT1ihKbbdJSt6A3TngIkacHJHi0SNUJmlpS0/Jg8Sn8kneKffQGjjpScODjZJ6LLs640zWlOGIg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/instrumentation': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-web': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/instrumentation@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-mIZil8Es+sYDK5m+DQiwAwF57F14TF2YlEqvIjZ/RQWcxDBwRGsKfdK2Tv65OU9meQKCMzSIFS9mxAcnAb6Bkg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/api-logs': 0.218.0
|
||||||
|
import-in-the-middle: 3.0.1
|
||||||
|
require-in-the-middle: 8.0.1
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/otlp-exporter-base@0.213.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-MegxAP1/n09Ob2dQvY5NBDVjAFkZRuKtWKxYev1R2M8hrsgXzQGkaMgoEKeUOyQ0FUyYcO29UOnYdQWmWa0PXg==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.213.0(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/otlp-exporter-base@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-ZwqpkNL5W7RyGJPDZ9g06DvKp8KFTWPJPN12anpMQYSKpTSU0z3EIZuPq9vPGpS8siFyOqDYDAuCwlNO9FqgbA==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/otlp-transformer': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/otlp-transformer@0.213.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-RSuAlxFFPjeK4d5Y6ps8L2WhaQI6CXWllIjvo5nkAlBpmq2XdYWEBGiAbOF4nDs8CX4QblJDv5BbMUft3sEfDw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/api-logs': 0.213.0
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-logs': 0.213.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-metrics': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
protobufjs: 7.6.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/otlp-transformer@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-CFaKH87WAzjuJ4awowTTLzUvMfaRfiOFG5+qm5S5ncyalRtN4ecQ+YmuANJSCrVPuvZFEkUgKhBPBndxi3rHsQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': ^1.3.0
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/api-logs': 0.218.0
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-logs': 0.218.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-metrics': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/resources@2.6.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-logs@0.213.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-00xlU3GZXo3kXKve4DLdrAL0NAFUaZ9appU/mn00S/5kSUdAvyYsORaDUfR04Mp2CLagAOhrzfUvYozY/EZX2g==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/api-logs': 0.213.0
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-logs@0.218.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-QvnNdugatFTVCJXH0Mcu7GOOJSylA9j127kIezOE4YwTI4YbowRons2K4WZTv5FMS8T4q9P0NdaRHdkSmeAIag==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.4.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/api-logs': 0.218.0
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-metrics@2.6.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-CicxWZxX6z35HR83jl+PLgtFgUrKRQ9LCXyxgenMnz5A1lgYWfAog7VtdOvGkJYyQgMNPhXQwkYrDLujk7z1Iw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-metrics@2.7.1(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-trace-base@2.6.0(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.6.0(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/semantic-conventions': 1.41.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/sdk-trace-web@2.7.1(@opentelemetry/api@1.9.1):
|
||||||
|
resolution: {integrity: sha512-K806OouCSOjMd8Nr7+ZCq3QT22tdAzzS/7h8vprfiKjkgFQ99/dvwU8d12WJANA6D5Qtme65hyBAqAu9CkQuxQ==}
|
||||||
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
'@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1)
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@opentelemetry/semantic-conventions@1.41.1:
|
||||||
|
resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@pkgjs/parseargs@0.11.0:
|
/@pkgjs/parseargs@0.11.0:
|
||||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -1454,6 +1829,48 @@ packages:
|
|||||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/aspromise@1.1.2:
|
||||||
|
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/base64@1.1.2:
|
||||||
|
resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/codegen@2.0.5:
|
||||||
|
resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/eventemitter@1.1.1:
|
||||||
|
resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/fetch@1.1.1:
|
||||||
|
resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==}
|
||||||
|
dependencies:
|
||||||
|
'@protobufjs/aspromise': 1.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/float@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/inquire@1.1.2:
|
||||||
|
resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/path@1.1.2:
|
||||||
|
resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/pool@1.1.0:
|
||||||
|
resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/@protobufjs/utf8@1.1.1:
|
||||||
|
resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@rolldown/pluginutils@1.0.0-beta.53:
|
/@rolldown/pluginutils@1.0.0-beta.53:
|
||||||
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -2004,7 +2421,7 @@ packages:
|
|||||||
browser-assert: 1.2.1
|
browser-assert: 1.2.1
|
||||||
storybook: 8.6.15(prettier@3.5.3)
|
storybook: 8.6.15(prettier@3.5.3)
|
||||||
ts-dedent: 2.2.0
|
ts-dedent: 2.2.0
|
||||||
vite: 5.4.17
|
vite: 5.4.17(@types/node@22.19.3)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@storybook/components@8.6.15(storybook@8.6.15):
|
/@storybook/components@8.6.15(storybook@8.6.15):
|
||||||
@@ -2146,7 +2563,7 @@ packages:
|
|||||||
resolve: 1.22.11
|
resolve: 1.22.11
|
||||||
storybook: 8.6.15(prettier@3.5.3)
|
storybook: 8.6.15(prettier@3.5.3)
|
||||||
tsconfig-paths: 4.2.0
|
tsconfig-paths: 4.2.0
|
||||||
vite: 5.4.17
|
vite: 5.4.17(@types/node@22.19.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -2333,7 +2750,7 @@ packages:
|
|||||||
'@tailwindcss/node': 4.1.18
|
'@tailwindcss/node': 4.1.18
|
||||||
'@tailwindcss/oxide': 4.1.18
|
'@tailwindcss/oxide': 4.1.18
|
||||||
tailwindcss: 4.1.18
|
tailwindcss: 4.1.18
|
||||||
vite: 5.4.17
|
vite: 5.4.17(@types/node@22.19.3)
|
||||||
|
|
||||||
/@tootallnate/once@2.0.0:
|
/@tootallnate/once@2.0.0:
|
||||||
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
|
||||||
@@ -3094,7 +3511,7 @@ packages:
|
|||||||
'@rolldown/pluginutils': 1.0.0-beta.53
|
'@rolldown/pluginutils': 1.0.0-beta.53
|
||||||
'@types/babel__core': 7.20.5
|
'@types/babel__core': 7.20.5
|
||||||
react-refresh: 0.18.0
|
react-refresh: 0.18.0
|
||||||
vite: 5.4.17
|
vite: 5.4.17(@types/node@22.19.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
@@ -3178,6 +3595,14 @@ packages:
|
|||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/acorn-import-attributes@1.9.5(acorn@8.15.0):
|
||||||
|
resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==}
|
||||||
|
peerDependencies:
|
||||||
|
acorn: ^8
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/acorn-jsx@5.3.2(acorn@8.14.1):
|
/acorn-jsx@5.3.2(acorn@8.14.1):
|
||||||
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3209,7 +3634,6 @@ packages:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/agent-base@7.1.4:
|
/agent-base@7.1.4:
|
||||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
@@ -3546,7 +3970,6 @@ packages:
|
|||||||
|
|
||||||
/asynckit@0.4.0:
|
/asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/at-least-node@1.0.0:
|
/at-least-node@1.0.0:
|
||||||
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==}
|
||||||
@@ -3564,6 +3987,18 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/axios@1.16.1:
|
||||||
|
resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==}
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.16.0
|
||||||
|
form-data: 4.0.5
|
||||||
|
https-proxy-agent: 5.0.1
|
||||||
|
proxy-from-env: 2.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/axobject-query@4.1.0:
|
/axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -3894,6 +4329,10 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/cjs-module-lexer@2.2.0:
|
||||||
|
resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/clean-regexp@1.0.0:
|
/clean-regexp@1.0.0:
|
||||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3986,7 +4425,6 @@ packages:
|
|||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/commander@5.1.0:
|
/commander@5.1.0:
|
||||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||||
@@ -4249,7 +4687,6 @@ packages:
|
|||||||
/delayed-stream@1.0.0:
|
/delayed-stream@1.0.0:
|
||||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/delegates@1.0.0:
|
/delegates@1.0.0:
|
||||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||||
@@ -5385,6 +5822,16 @@ packages:
|
|||||||
/flatted@3.3.3:
|
/flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
/follow-redirects@1.16.0:
|
||||||
|
resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/for-each@0.3.5:
|
/for-each@0.3.5:
|
||||||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -5407,7 +5854,6 @@ packages:
|
|||||||
es-set-tostringtag: 2.1.0
|
es-set-tostringtag: 2.1.0
|
||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.18
|
mime-types: 2.1.18
|
||||||
dev: true
|
|
||||||
|
|
||||||
/fs-constants@1.0.0:
|
/fs-constants@1.0.0:
|
||||||
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
@@ -5723,6 +6169,12 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
/hoist-non-react-statics@3.3.2:
|
||||||
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
dependencies:
|
||||||
|
react-is: 16.13.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/hosted-git-info@2.8.9:
|
/hosted-git-info@2.8.9:
|
||||||
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5782,7 +6234,6 @@ packages:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
|
||||||
|
|
||||||
/https-proxy-agent@7.0.6:
|
/https-proxy-agent@7.0.6:
|
||||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||||
@@ -5844,6 +6295,16 @@ packages:
|
|||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
resolve-from: 4.0.0
|
resolve-from: 4.0.0
|
||||||
|
|
||||||
|
/import-in-the-middle@3.0.1:
|
||||||
|
resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
dependencies:
|
||||||
|
acorn: 8.15.0
|
||||||
|
acorn-import-attributes: 1.9.5(acorn@8.15.0)
|
||||||
|
cjs-module-lexer: 2.2.0
|
||||||
|
module-details-from-path: 1.0.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/import-meta-resolve@4.2.0:
|
/import-meta-resolve@4.2.0:
|
||||||
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
|
resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -6540,6 +7001,10 @@ packages:
|
|||||||
is-unicode-supported: 0.1.0
|
is-unicode-supported: 0.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/long@5.3.2:
|
||||||
|
resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/longest-streak@3.1.0:
|
/longest-streak@3.1.0:
|
||||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -7021,7 +7486,6 @@ packages:
|
|||||||
/mime-db@1.33.0:
|
/mime-db@1.33.0:
|
||||||
resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
|
resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime-db@1.54.0:
|
/mime-db@1.54.0:
|
||||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||||
@@ -7033,7 +7497,6 @@ packages:
|
|||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db: 1.33.0
|
mime-db: 1.33.0
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mime@2.6.0:
|
/mime@2.6.0:
|
||||||
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==}
|
||||||
@@ -7165,6 +7628,10 @@ packages:
|
|||||||
hasBin: true
|
hasBin: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/module-details-from-path@1.0.4:
|
||||||
|
resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mri@1.2.0:
|
/mri@1.2.0:
|
||||||
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -7712,6 +8179,30 @@ packages:
|
|||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/protobufjs@7.6.1:
|
||||||
|
resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
requiresBuild: true
|
||||||
|
dependencies:
|
||||||
|
'@protobufjs/aspromise': 1.1.2
|
||||||
|
'@protobufjs/base64': 1.1.2
|
||||||
|
'@protobufjs/codegen': 2.0.5
|
||||||
|
'@protobufjs/eventemitter': 1.1.1
|
||||||
|
'@protobufjs/fetch': 1.1.1
|
||||||
|
'@protobufjs/float': 1.0.2
|
||||||
|
'@protobufjs/inquire': 1.1.2
|
||||||
|
'@protobufjs/path': 1.1.2
|
||||||
|
'@protobufjs/pool': 1.1.0
|
||||||
|
'@protobufjs/utf8': 1.1.1
|
||||||
|
'@types/node': 22.19.3
|
||||||
|
long: 5.3.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/proxy-from-env@2.1.0:
|
||||||
|
resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/pump@3.0.4:
|
/pump@3.0.4:
|
||||||
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8059,6 +8550,16 @@ packages:
|
|||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/require-in-the-middle@8.0.1:
|
||||||
|
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
|
||||||
|
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
module-details-from-path: 1.0.4
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
dev: false
|
||||||
|
|
||||||
/requireindex@1.2.0:
|
/requireindex@1.2.0:
|
||||||
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
resolution: {integrity: sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==}
|
||||||
engines: {node: '>=0.10.5'}
|
engines: {node: '>=0.10.5'}
|
||||||
@@ -9111,6 +9612,11 @@ packages:
|
|||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
/ua-parser-js@1.0.41:
|
||||||
|
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/unbox-primitive@1.1.0:
|
/unbox-primitive@1.1.0:
|
||||||
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -9457,43 +9963,6 @@ packages:
|
|||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/vite@5.4.17:
|
|
||||||
resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
|
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
|
||||||
hasBin: true
|
|
||||||
peerDependencies:
|
|
||||||
'@types/node': ^18.0.0 || >=20.0.0
|
|
||||||
less: '*'
|
|
||||||
lightningcss: ^1.21.0
|
|
||||||
sass: '*'
|
|
||||||
sass-embedded: '*'
|
|
||||||
stylus: '*'
|
|
||||||
sugarss: '*'
|
|
||||||
terser: ^5.4.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@types/node':
|
|
||||||
optional: true
|
|
||||||
less:
|
|
||||||
optional: true
|
|
||||||
lightningcss:
|
|
||||||
optional: true
|
|
||||||
sass:
|
|
||||||
optional: true
|
|
||||||
sass-embedded:
|
|
||||||
optional: true
|
|
||||||
stylus:
|
|
||||||
optional: true
|
|
||||||
sugarss:
|
|
||||||
optional: true
|
|
||||||
terser:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
esbuild: 0.21.5
|
|
||||||
postcss: 8.5.3
|
|
||||||
rollup: 4.39.0
|
|
||||||
optionalDependencies:
|
|
||||||
fsevents: 2.3.3
|
|
||||||
|
|
||||||
/vite@5.4.17(@types/node@22.19.3):
|
/vite@5.4.17(@types/node@22.19.3):
|
||||||
resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
|
resolution: {integrity: sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==}
|
||||||
engines: {node: ^18.0.0 || >=20.0.0}
|
engines: {node: ^18.0.0 || >=20.0.0}
|
||||||
@@ -9527,11 +9996,10 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.3
|
'@types/node': 22.19.3
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.6
|
postcss: 8.5.3
|
||||||
rollup: 4.55.1
|
rollup: 4.39.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
|
||||||
|
|
||||||
/vite@7.3.1:
|
/vite@7.3.1:
|
||||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||||
@@ -9583,7 +10051,7 @@ packages:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest@4.0.17:
|
/vitest@4.0.17(@opentelemetry/api@1.9.1):
|
||||||
resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==}
|
resolution: {integrity: sha512-FQMeF0DJdWY0iOnbv466n/0BudNdKj1l5jYgl5JVTwjSsZSlqyXFt/9+1sEyhR6CLowbZpV7O1sCHrzBhucKKg==}
|
||||||
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -9617,6 +10085,7 @@ packages:
|
|||||||
jsdom:
|
jsdom:
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@opentelemetry/api': 1.9.1
|
||||||
'@vitest/expect': 4.0.17
|
'@vitest/expect': 4.0.17
|
||||||
'@vitest/mocker': 4.0.17(vite@7.3.1)
|
'@vitest/mocker': 4.0.17(vite@7.3.1)
|
||||||
'@vitest/pretty-format': 4.0.17
|
'@vitest/pretty-format': 4.0.17
|
||||||
@@ -9661,6 +10130,10 @@ packages:
|
|||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/web-vitals@5.2.0:
|
||||||
|
resolution: {integrity: sha512-i2z98bEmaCqSDiHEDu+gHl/dmR4Q+TxFmG3/13KkMO+o8UxQzCqWaDRCiLgEa41nlO4VpXSI0ASa1xWmO9sBlA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/webpack-virtual-modules@0.6.2:
|
/webpack-virtual-modules@0.6.2:
|
||||||
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|||||||
Reference in New Issue
Block a user