ionic-planb-logistic-app-fl.../lib/api/client.dart

424 lines
13 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http_interceptor/http_interceptor.dart';
import 'types.dart';
import 'openapi_config.dart';
import '../utils/logging_interceptor.dart';
import '../utils/http_client_factory.dart';
import '../services/auth_service.dart';
class CqrsApiClient {
final ApiClientConfig config;
final AuthService? authService;
late final http.Client _httpClient;
CqrsApiClient({
required this.config,
this.authService,
http.Client? httpClient,
}) {
_httpClient = httpClient ?? InterceptedClient.build(
interceptors: [LoggingInterceptor()],
client: HttpClientFactory.createClient(
allowSelfSigned: config.allowSelfSignedCertificate,
),
);
}
String get baseUrl => config.baseUrl;
Future<Result<T>> executeQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) fromJson,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/query/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(query.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeQuery(
endpoint: endpoint,
query: query,
fromJson: fromJson,
isRetry: true,
),
onError: (error) => _handleResponse<T>(response, fromJson),
cancelled: () => _handleResponse<T>(response, fromJson),
);
}
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<PaginatedResult<T>>> executePaginatedQuery<T>({
required String endpoint,
required Serializable query,
required T Function(Map<String, dynamic>) itemFromJson,
required int page,
required int pageSize,
List<FilterCriteria>? filters,
bool isRetry = false,
}) async {
try {
final url = Uri.parse(
'$baseUrl/api/query/$endpoint?page=$page&pageSize=$pageSize',
);
final headers = await _buildHeaders();
final queryData = {
...query.toJson(),
if (filters != null && filters.isNotEmpty)
'filters': filters.map((f) => f.toJson()).toList(),
};
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(queryData),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executePaginatedQuery(
endpoint: endpoint,
query: query,
itemFromJson: itemFromJson,
page: page,
pageSize: pageSize,
filters: filters,
isRetry: true,
),
onError: (error) => _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize),
cancelled: () => _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize),
);
}
return _handlePaginatedResponse<T>(response, itemFromJson, page, pageSize);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute paginated query: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<void>> executeCommand({
required String endpoint,
required Serializable command,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeCommand(
endpoint: endpoint,
command: command,
isRetry: true,
),
onError: (error) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
},
cancelled: () {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
},
);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(null);
} else {
return _handleErrorResponse(response);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<T>> executeCommandWithResult<T>({
required String endpoint,
required Serializable command,
required T Function(Map<String, dynamic>) fromJson,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final response = await _httpClient
.post(
url,
headers: headers,
body: jsonEncode(command.toJson()),
)
.timeout(config.timeout);
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => executeCommandWithResult(
endpoint: endpoint,
command: command,
fromJson: fromJson,
isRetry: true,
),
onError: (error) => _handleResponse<T>(response, fromJson),
cancelled: () => _handleResponse<T>(response, fromJson),
);
}
return _handleResponse<T>(response, fromJson);
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to execute command with result: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Result<String>> uploadFile({
required String endpoint,
required String filePath,
required String fieldName,
Map<String, String>? additionalFields,
bool isRetry = false,
}) async {
try {
final url = Uri.parse('$baseUrl/api/command/$endpoint');
final headers = await _buildHeaders();
final request = http.MultipartRequest('POST', url)
..headers.addAll(headers)
..files.add(await http.MultipartFile.fromPath(fieldName, filePath));
if (additionalFields != null) {
request.fields.addAll(additionalFields);
}
final response = await request.send().timeout(config.timeout);
final responseBody = await response.stream.bytesToString();
if (response.statusCode == 401 && !isRetry && authService != null) {
final refreshResult = await authService!.refreshAccessToken();
return refreshResult.when(
success: (token) => uploadFile(
endpoint: endpoint,
filePath: filePath,
fieldName: fieldName,
additionalFields: additionalFields,
isRetry: true,
),
onError: (error) {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
},
cancelled: () {
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
},
);
}
if (response.statusCode >= 200 && response.statusCode < 300) {
return Result.success(responseBody);
} else {
return _parseErrorFromString(responseBody, response.statusCode);
}
} on TimeoutException {
return Result.error(ApiError.timeout());
} catch (e, stackTrace) {
return Result.error(
ApiError.unknown(
'Failed to upload file: ${e.toString()}',
exception: Exception(stackTrace.toString()),
),
);
}
}
Future<Map<String, String>> _buildHeaders() async {
final headers = <String, String>{
'Content-Type': 'application/json',
'Accept': 'application/json',
...config.defaultHeaders,
};
if (authService != null) {
// Proactively ensure token is valid and refresh if needed
final token = await authService!.ensureValidToken();
if (token != null) {
headers['Authorization'] = 'Bearer $token';
}
}
return headers;
}
Result<T> _handleResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) fromJson,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
if (response.body.isEmpty) {
return Result.success(null as T);
}
final data = jsonDecode(response.body) as Map<String, dynamic>;
return Result.success(fromJson(data));
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse response: ${e.toString()}'),
);
}
}
Result<PaginatedResult<T>> _handlePaginatedResponse<T>(
http.Response response,
T Function(Map<String, dynamic>) itemFromJson,
int page,
int pageSize,
) {
try {
if (response.statusCode >= 200 && response.statusCode < 300) {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final items = (data['items'] as List?)
?.map((item) => itemFromJson(item as Map<String, dynamic>))
.toList() ?? [];
final totalCount = data['totalCount'] as int? ?? items.length;
return Result.success(
PaginatedResult(
items: items,
page: page,
pageSize: pageSize,
totalCount: totalCount,
),
);
} else {
return _handleErrorResponse(response);
}
} catch (e) {
return Result.error(
ApiError.unknown('Failed to parse paginated response: ${e.toString()}'),
);
}
}
Result<Never> _handleErrorResponse(http.Response response) {
try {
final data = jsonDecode(response.body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
if (response.statusCode == 422) {
final errors = data['errors'] as Map<String, dynamic>?;
final details = errors?.map(
(key, value) => MapEntry(
key,
(value as List?)?.map((e) => e.toString()).toList() ?? [],
),
);
return Result.error(
ApiError.validation(message, details),
);
}
return Result.error(
ApiError.http(statusCode: response.statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(
statusCode: response.statusCode,
message: response.reasonPhrase ?? 'Unknown error',
),
);
}
}
Result<Never> _parseErrorFromString(String body, int statusCode) {
try {
final data = jsonDecode(body) as Map<String, dynamic>;
final message = data['message'] as String? ?? 'An error occurred';
return Result.error(
ApiError.http(statusCode: statusCode, message: message),
);
} catch (e) {
return Result.error(
ApiError.http(statusCode: statusCode, message: 'Unknown error'),
);
}
}
void close() {
_httpClient.close();
}
}