CODEX_ADK/FRONTEND/lib/api/client.dart
jean-philippe 3fae2fcbe1 Initial commit: CODEX_ADK (Svrnty Console) MVP v1.0.0
This is the initial commit for the CODEX_ADK project, a full-stack AI agent
management platform featuring:

BACKEND (ASP.NET Core 8.0):
- CQRS architecture with 6 commands and 7 queries
- 16 API endpoints (all working and tested)
- PostgreSQL database with 5 entities
- AES-256 encryption for API keys
- FluentValidation on all commands
- Rate limiting and CORS configured
- OpenAPI/Swagger documentation
- Docker Compose setup (PostgreSQL + Ollama)

FRONTEND (Flutter 3.x):
- Dark theme with Svrnty branding
- Collapsible sidebar navigation
- CQRS API client with Result<T> error handling
- Type-safe endpoints from OpenAPI schema
- Multi-platform support (Web, iOS, Android, macOS, Linux, Windows)

DOCUMENTATION:
- Comprehensive API reference
- Architecture documentation
- Development guidelines for Claude Code
- API integration guides
- context-claude.md project overview

Status: Backend ready (Grade A-), Frontend integration pending

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 18:32:38 -04:00

402 lines
12 KiB
Dart

/// CQRS API Client for communicating with the backend
library;
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'types.dart';
// =============================================================================
// API Client Configuration
// =============================================================================
/// Configuration for the API client
class ApiClientConfig {
final String baseUrl;
final Duration timeout;
final Map<String, String> defaultHeaders;
const ApiClientConfig({
required this.baseUrl,
this.timeout = const Duration(seconds: 30),
this.defaultHeaders = const {},
});
/// Default configuration for local development
static const ApiClientConfig development = ApiClientConfig(
baseUrl: 'http://localhost:5246',
timeout: Duration(seconds: 30),
defaultHeaders: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
);
}
// =============================================================================
// CQRS API Client
// =============================================================================
/// HTTP client for CQRS-based API
///
/// Provides methods for executing:
/// - Queries (read operations)
/// - Commands (write operations)
/// - Paginated queries (list operations with filtering/sorting)
class CqrsApiClient {
final ApiClientConfig config;
final http.Client _httpClient;
CqrsApiClient({
required this.config,
http.Client? httpClient,
}) : _httpClient = httpClient ?? http.Client();
/// Execute a query that returns a single value
///
/// Example:
/// ```dart
/// final result = await client.executeQuery<bool>(
/// 'health',
/// HealthQuery(),
/// (json) => json as bool,
/// );
/// ```
Future<Result<TResult>> executeQuery<TResult>({
required String endpoint,
required Serializable query,
required TResult Function(Object? json) fromJson,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/query/$endpoint');
final body = _serializeQuery(query);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handleResponse<TResult>(response, fromJson);
} on TimeoutException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
/// Execute a paginated query that returns a list of items
///
/// Example:
/// ```dart
/// final result = await client.executePaginatedQuery<VehicleItem>(
/// 'vehicles',
/// VehiclesQuery(),
/// (json) => VehicleItem.fromJson(json as Map<String, Object?>),
/// page: 1,
/// pageSize: 20,
/// filters: [FilterCriteria(field: 'status', operator: FilterOperator.equals, value: 'active')],
/// sorting: [SortCriteria(field: 'createdAt', direction: SortDirection.descending)],
/// );
/// ```
Future<Result<PaginatedResponse<TItem>>> executePaginatedQuery<TItem>({
required String endpoint,
required Serializable query,
required TItem Function(Map<String, Object?> json) itemFromJson,
int page = 1,
int pageSize = 20,
List<FilterCriteria>? filters,
List<SortCriteria>? sorting,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/query/$endpoint');
// Build request body with query + pagination/filtering/sorting
final queryMap = _queryToMap(query);
final requestBody = <String, Object?>{
...queryMap,
'page': page,
'pageSize': pageSize,
if (filters != null && filters.isNotEmpty)
'filters': filters.map((f) => f.toJson()).toList(),
if (sorting != null && sorting.isNotEmpty)
'sorts': sorting.map((s) => s.toJson()).toList(),
};
final body = jsonEncode(requestBody);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handlePaginatedResponse<TItem>(response, itemFromJson);
} on TimeoutException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
/// Execute a command (write operation)
///
/// Example:
/// ```dart
/// final result = await client.executeCommand(
/// 'createVehicle',
/// CreateVehicleCommand(name: 'Tesla Model 3'),
/// );
/// ```
Future<Result<void>> executeCommand({
required String endpoint,
required Serializable command,
}) async {
try {
final url = Uri.parse('${config.baseUrl}/api/command/$endpoint');
final body = _serializeQuery(command);
final response = await _httpClient
.post(
url,
headers: config.defaultHeaders,
body: body,
)
.timeout(config.timeout);
return _handleCommandResponse(response);
} on TimeoutException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Request timeout: ${e.message ?? "Operation took too long"}',
type: ApiErrorType.timeout,
),
);
} on SocketException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Network error: ${e.message}',
type: ApiErrorType.network,
details: e.osError?.message,
),
);
} on http.ClientException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'HTTP client error: ${e.message}',
type: ApiErrorType.http,
),
);
} on FormatException catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'JSON parsing error: ${e.message}',
type: ApiErrorType.serialization,
details: e.source.toString(),
),
);
} catch (e) {
return ApiError<void>(
ApiErrorInfo(
message: 'Unexpected error: $e',
type: ApiErrorType.unknown,
),
);
}
}
// ---------------------------------------------------------------------------
// Private Helper Methods
// ---------------------------------------------------------------------------
/// Serialize query/command object to JSON string
String _serializeQuery(Serializable query) {
final jsonMap = query.toJson();
return jsonEncode(jsonMap);
}
/// Convert query object to Map for pagination requests
Map<String, Object?> _queryToMap(Serializable query) {
return query.toJson();
}
/// Handle HTTP response for single value queries
Result<TResult> _handleResponse<TResult>(
http.Response response,
TResult Function(Object? json) fromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
try {
final json = jsonDecode(response.body);
final result = fromJson(json);
return ApiSuccess<TResult>(result);
} catch (e) {
return ApiError<TResult>(
ApiErrorInfo(
message: 'Failed to parse response',
statusCode: response.statusCode,
type: ApiErrorType.serialization,
details: e.toString(),
),
);
}
} else {
return ApiError<TResult>(
_parseErrorResponse(response),
);
}
}
/// Handle HTTP response for paginated queries
Result<PaginatedResponse<TItem>> _handlePaginatedResponse<TItem>(
http.Response response,
TItem Function(Map<String, Object?>) itemFromJson,
) {
if (response.statusCode >= 200 && response.statusCode < 300) {
try {
final json = jsonDecode(response.body) as Map<String, Object?>;
final paginatedResponse = PaginatedResponse<TItem>.fromJson(
json,
itemFromJson,
);
return ApiSuccess<PaginatedResponse<TItem>>(paginatedResponse);
} catch (e) {
return ApiError<PaginatedResponse<TItem>>(
ApiErrorInfo(
message: 'Failed to parse paginated response',
statusCode: response.statusCode,
type: ApiErrorType.serialization,
details: e.toString(),
),
);
}
} else {
return ApiError<PaginatedResponse<TItem>>(
_parseErrorResponse(response),
);
}
}
/// Handle HTTP response for commands
Result<void> _handleCommandResponse(http.Response response) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return const ApiSuccess<void>(null);
} else {
return ApiError<void>(_parseErrorResponse(response));
}
}
/// Parse error information from HTTP response
ApiErrorInfo _parseErrorResponse(http.Response response) {
String message = 'Request failed with status ${response.statusCode}';
String? details;
try {
final json = jsonDecode(response.body) as Map<String, Object?>;
message = json['message'] as String? ?? message;
details = json['details'] as String? ?? json['error'] as String?;
} catch (_) {
// If JSON parsing fails, use response body as details
details = response.body;
}
return ApiErrorInfo(
message: message,
statusCode: response.statusCode,
type: _determineErrorType(response.statusCode),
details: details,
);
}
/// Determine error type based on HTTP status code
ApiErrorType _determineErrorType(int statusCode) {
if (statusCode >= 400 && statusCode < 500) {
if (statusCode == 422) {
return ApiErrorType.validation;
}
return ApiErrorType.http;
} else if (statusCode >= 500) {
return ApiErrorType.http;
}
return ApiErrorType.unknown;
}
/// Dispose of resources
void dispose() {
_httpClient.close();
}
}