mirror of
https://github.com/foss42/apidash.git
synced 2025-12-01 10:17:47 +08:00
Refactored collection state management to handle API type changes and AI request models. Updated widgets and tests to support nullable HTTP methods and AI request models, and improved response body rendering for AI responses.
598 lines
18 KiB
Dart
598 lines
18 KiB
Dart
import 'dart:async';
|
|
import 'package:apidash_core/apidash_core.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:apidash/consts.dart';
|
|
import 'providers.dart';
|
|
import '../models/models.dart';
|
|
import '../services/services.dart';
|
|
import '../utils/utils.dart';
|
|
|
|
final selectedIdStateProvider = StateProvider<String?>((ref) => null);
|
|
|
|
final selectedRequestModelProvider = StateProvider<RequestModel?>((ref) {
|
|
final selectedId = ref.watch(selectedIdStateProvider);
|
|
final collection = ref.watch(collectionStateNotifierProvider);
|
|
if (selectedId == null || collection == null) {
|
|
return null;
|
|
} else {
|
|
return collection[selectedId];
|
|
}
|
|
});
|
|
|
|
final requestSequenceProvider = StateProvider<List<String>>((ref) {
|
|
var ids = hiveHandler.getIds();
|
|
return ids ?? [];
|
|
});
|
|
|
|
final StateNotifierProvider<CollectionStateNotifier, Map<String, RequestModel>?>
|
|
collectionStateNotifierProvider =
|
|
StateNotifierProvider((ref) => CollectionStateNotifier(
|
|
ref,
|
|
hiveHandler,
|
|
));
|
|
|
|
class CollectionStateNotifier
|
|
extends StateNotifier<Map<String, RequestModel>?> {
|
|
CollectionStateNotifier(
|
|
this.ref,
|
|
this.hiveHandler,
|
|
) : super(null) {
|
|
var status = loadData();
|
|
Future.microtask(() {
|
|
if (status) {
|
|
ref.read(requestSequenceProvider.notifier).state = [
|
|
state!.keys.first,
|
|
];
|
|
}
|
|
ref.read(selectedIdStateProvider.notifier).state =
|
|
ref.read(requestSequenceProvider)[0];
|
|
});
|
|
}
|
|
|
|
final Ref ref;
|
|
final HiveHandler hiveHandler;
|
|
final baseHttpResponseModel = const HttpResponseModel();
|
|
|
|
bool hasId(String id) => state?.keys.contains(id) ?? false;
|
|
|
|
RequestModel? getRequestModel(String id) {
|
|
return state?[id];
|
|
}
|
|
|
|
void unsave() {
|
|
ref.read(hasUnsavedChangesProvider.notifier).state = true;
|
|
}
|
|
|
|
void add() {
|
|
final id = getNewUuid();
|
|
final newRequestModel = RequestModel(
|
|
id: id,
|
|
httpRequestModel: const HttpRequestModel(),
|
|
);
|
|
var map = {...state!};
|
|
map[id] = newRequestModel;
|
|
state = map;
|
|
ref
|
|
.read(requestSequenceProvider.notifier)
|
|
.update((state) => [id, ...state]);
|
|
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
|
|
unsave();
|
|
}
|
|
|
|
void addRequestModel(
|
|
HttpRequestModel httpRequestModel, {
|
|
String? name,
|
|
}) {
|
|
final id = getNewUuid();
|
|
final newRequestModel = RequestModel(
|
|
id: id,
|
|
name: name ?? "",
|
|
httpRequestModel: httpRequestModel,
|
|
);
|
|
var map = {...state!};
|
|
map[id] = newRequestModel;
|
|
state = map;
|
|
ref
|
|
.read(requestSequenceProvider.notifier)
|
|
.update((state) => [id, ...state]);
|
|
ref.read(selectedIdStateProvider.notifier).state = newRequestModel.id;
|
|
unsave();
|
|
}
|
|
|
|
void reorder(int oldIdx, int newIdx) {
|
|
var itemIds = ref.read(requestSequenceProvider);
|
|
final itemId = itemIds.removeAt(oldIdx);
|
|
itemIds.insert(newIdx, itemId);
|
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
|
unsave();
|
|
}
|
|
|
|
void remove({String? id}) {
|
|
final rId = id ?? ref.read(selectedIdStateProvider);
|
|
var itemIds = ref.read(requestSequenceProvider);
|
|
int idx = itemIds.indexOf(rId!);
|
|
cancelHttpRequest(rId);
|
|
itemIds.remove(rId);
|
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
|
|
|
String? newId;
|
|
if (idx == 0 && itemIds.isNotEmpty) {
|
|
newId = itemIds[0];
|
|
} else if (itemIds.length > 1) {
|
|
newId = itemIds[idx - 1];
|
|
} else {
|
|
newId = null;
|
|
}
|
|
|
|
ref.read(selectedIdStateProvider.notifier).state = newId;
|
|
|
|
var map = {...state!};
|
|
map.remove(rId);
|
|
state = map;
|
|
unsave();
|
|
}
|
|
|
|
void clearResponse({String? id}) {
|
|
final rId = id ?? ref.read(selectedIdStateProvider);
|
|
if (rId == null || state?[rId] == null) return;
|
|
var currentModel = state![rId]!;
|
|
final newModel = currentModel.copyWith(
|
|
responseStatus: null,
|
|
message: null,
|
|
httpResponseModel: null,
|
|
isWorking: false,
|
|
sendingTime: null,
|
|
);
|
|
var map = {...state!};
|
|
map[rId] = newModel;
|
|
state = map;
|
|
unsave();
|
|
}
|
|
|
|
void duplicate({String? id}) {
|
|
final rId = id ?? ref.read(selectedIdStateProvider);
|
|
final newId = getNewUuid();
|
|
|
|
var itemIds = ref.read(requestSequenceProvider);
|
|
int idx = itemIds.indexOf(rId!);
|
|
var currentModel = state![rId]!;
|
|
final newModel = currentModel.copyWith(
|
|
id: newId,
|
|
name: "${currentModel.name} (copy)",
|
|
requestTabIndex: 0,
|
|
responseStatus: null,
|
|
message: null,
|
|
httpRequestModel: currentModel.httpRequestModel?.copyWith(),
|
|
aiRequestModel: currentModel.aiRequestModel?.copyWith(),
|
|
httpResponseModel: null,
|
|
isWorking: false,
|
|
sendingTime: null,
|
|
);
|
|
|
|
itemIds.insert(idx + 1, newId);
|
|
var map = {...state!};
|
|
map[newId] = newModel;
|
|
state = map;
|
|
|
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
|
ref.read(selectedIdStateProvider.notifier).state = newId;
|
|
unsave();
|
|
}
|
|
|
|
void duplicateFromHistory(HistoryRequestModel historyRequestModel) {
|
|
final newId = getNewUuid();
|
|
|
|
var itemIds = ref.read(requestSequenceProvider);
|
|
var currentModel = historyRequestModel;
|
|
|
|
final newModel = RequestModel(
|
|
apiType: currentModel.metaData.apiType,
|
|
id: newId,
|
|
name: "${currentModel.metaData.name} (history)",
|
|
aiRequestModel: currentModel.aiRequestModel?.copyWith(),
|
|
httpRequestModel:
|
|
currentModel.httpRequestModel?.copyWith() ?? HttpRequestModel(),
|
|
responseStatus: currentModel.metaData.responseStatus,
|
|
message: kResponseCodeReasons[currentModel.metaData.responseStatus],
|
|
httpResponseModel: currentModel.httpResponseModel,
|
|
isWorking: false,
|
|
sendingTime: null,
|
|
);
|
|
|
|
itemIds.insert(0, newId);
|
|
var map = {...state!};
|
|
map[newId] = newModel;
|
|
state = map;
|
|
|
|
ref.read(requestSequenceProvider.notifier).state = [...itemIds];
|
|
ref.read(selectedIdStateProvider.notifier).state = newId;
|
|
unsave();
|
|
}
|
|
|
|
void update({
|
|
APIType? apiType,
|
|
String? id,
|
|
HTTPVerb? method,
|
|
AuthModel? authModel,
|
|
String? url,
|
|
String? name,
|
|
String? description,
|
|
int? requestTabIndex,
|
|
List<NameValueModel>? headers,
|
|
List<NameValueModel>? params,
|
|
List<bool>? isHeaderEnabledList,
|
|
List<bool>? isParamEnabledList,
|
|
ContentType? bodyContentType,
|
|
String? body,
|
|
String? query,
|
|
List<FormDataModel>? formData,
|
|
int? responseStatus,
|
|
String? message,
|
|
HttpResponseModel? httpResponseModel,
|
|
String? preRequestScript,
|
|
String? postRequestScript,
|
|
AIRequestModel? aiRequestModel,
|
|
}) {
|
|
final rId = id ?? ref.read(selectedIdStateProvider);
|
|
if (rId == null) {
|
|
debugPrint("Unable to update as Request Id is null");
|
|
return;
|
|
}
|
|
var currentModel = state![rId]!;
|
|
var currentHttpRequestModel = currentModel.httpRequestModel;
|
|
|
|
RequestModel newModel;
|
|
|
|
if (apiType != null && currentModel.apiType != apiType) {
|
|
newModel = switch (apiType) {
|
|
APIType.rest || APIType.graphql => currentModel.copyWith(
|
|
apiType: apiType,
|
|
name: name ?? currentModel.name,
|
|
description: description ?? currentModel.description,
|
|
httpRequestModel: const HttpRequestModel(),
|
|
aiRequestModel: null),
|
|
APIType.ai => currentModel.copyWith(
|
|
apiType: apiType,
|
|
name: name ?? currentModel.name,
|
|
description: description ?? currentModel.description,
|
|
httpRequestModel: null,
|
|
aiRequestModel: const AIRequestModel()),
|
|
};
|
|
} else {
|
|
newModel = currentModel.copyWith(
|
|
apiType: apiType ?? currentModel.apiType,
|
|
name: name ?? currentModel.name,
|
|
description: description ?? currentModel.description,
|
|
requestTabIndex: requestTabIndex ?? currentModel.requestTabIndex,
|
|
httpRequestModel: currentHttpRequestModel?.copyWith(
|
|
method: method ?? currentHttpRequestModel.method,
|
|
url: url ?? currentHttpRequestModel.url,
|
|
headers: headers ?? currentHttpRequestModel.headers,
|
|
params: params ?? currentHttpRequestModel.params,
|
|
authModel: authModel ?? currentHttpRequestModel.authModel,
|
|
isHeaderEnabledList: isHeaderEnabledList ??
|
|
currentHttpRequestModel.isHeaderEnabledList,
|
|
isParamEnabledList:
|
|
isParamEnabledList ?? currentHttpRequestModel.isParamEnabledList,
|
|
bodyContentType:
|
|
bodyContentType ?? currentHttpRequestModel.bodyContentType,
|
|
body: body ?? currentHttpRequestModel.body,
|
|
query: query ?? currentHttpRequestModel.query,
|
|
formData: formData ?? currentHttpRequestModel.formData,
|
|
),
|
|
responseStatus: responseStatus ?? currentModel.responseStatus,
|
|
message: message ?? currentModel.message,
|
|
httpResponseModel: httpResponseModel ?? currentModel.httpResponseModel,
|
|
preRequestScript: preRequestScript ?? currentModel.preRequestScript,
|
|
postRequestScript: postRequestScript ?? currentModel.postRequestScript,
|
|
aiRequestModel: aiRequestModel ?? currentModel.aiRequestModel,
|
|
);
|
|
}
|
|
|
|
var map = {...state!};
|
|
map[rId] = newModel;
|
|
state = map;
|
|
unsave();
|
|
}
|
|
|
|
Future<void> sendRequest() async {
|
|
final requestId = ref.read(selectedIdStateProvider);
|
|
ref.read(codePaneVisibleStateProvider.notifier).state = false;
|
|
|
|
if (requestId == null || state == null) {
|
|
return;
|
|
}
|
|
|
|
RequestModel? requestModel = state![requestId];
|
|
if (requestModel?.httpRequestModel == null &&
|
|
requestModel?.aiRequestModel == null) {
|
|
return;
|
|
}
|
|
|
|
final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme;
|
|
final EnvironmentModel? originalEnvironmentModel =
|
|
ref.read(activeEnvironmentModelProvider);
|
|
|
|
RequestModel executionRequestModel = requestModel!.copyWith();
|
|
|
|
if (!requestModel.preRequestScript.isNullOrEmpty()) {
|
|
executionRequestModel = await handlePreRequestScript(
|
|
executionRequestModel,
|
|
originalEnvironmentModel,
|
|
(envModel, updatedValues) {
|
|
ref
|
|
.read(environmentsStateNotifierProvider.notifier)
|
|
.updateEnvironment(
|
|
envModel.id,
|
|
name: envModel.name,
|
|
values: updatedValues,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
APIType apiType = executionRequestModel.apiType;
|
|
bool noSSL = ref.read(settingsProvider).isSSLDisabled;
|
|
HttpRequestModel substitutedHttpRequestModel;
|
|
|
|
if (apiType == APIType.ai) {
|
|
substitutedHttpRequestModel = getSubstitutedHttpRequestModel(
|
|
executionRequestModel.aiRequestModel!.httpRequestModel!);
|
|
} else {
|
|
substitutedHttpRequestModel = getSubstitutedHttpRequestModel(
|
|
executionRequestModel.httpRequestModel!);
|
|
}
|
|
|
|
// Set model to working and streaming
|
|
state = {
|
|
...state!,
|
|
requestId: requestModel.copyWith(
|
|
isWorking: true,
|
|
sendingTime: DateTime.now(),
|
|
),
|
|
};
|
|
bool streamingMode = true; //Default: Streaming First
|
|
|
|
final stream = await streamHttpRequest(
|
|
requestId,
|
|
apiType,
|
|
substitutedHttpRequestModel,
|
|
defaultUriScheme: defaultUriScheme,
|
|
noSSL: noSSL,
|
|
);
|
|
|
|
HttpResponseModel? httpResponseModel;
|
|
HistoryRequestModel? historyModel;
|
|
RequestModel newRequestModel = requestModel;
|
|
bool isStreamingResponse = false;
|
|
final completer = Completer<(Response?, Duration?, String?)>();
|
|
|
|
StreamSubscription? sub;
|
|
|
|
sub = stream.listen((rec) async {
|
|
if (rec == null) return;
|
|
|
|
isStreamingResponse = rec.$1 ?? false;
|
|
final response = rec.$2;
|
|
final duration = rec.$3;
|
|
final errorMessage = rec.$4;
|
|
|
|
if (isStreamingResponse) {
|
|
httpResponseModel = httpResponseModel?.copyWith(
|
|
time: duration,
|
|
sseOutput: [
|
|
...(httpResponseModel?.sseOutput ?? []),
|
|
if (response != null) response.body,
|
|
],
|
|
);
|
|
|
|
newRequestModel = newRequestModel.copyWith(
|
|
httpResponseModel: httpResponseModel,
|
|
isStreaming: true,
|
|
);
|
|
state = {
|
|
...state!,
|
|
requestId: newRequestModel,
|
|
};
|
|
unsave();
|
|
|
|
if (historyModel != null && httpResponseModel != null) {
|
|
historyModel =
|
|
historyModel!.copyWith(httpResponseModel: httpResponseModel!);
|
|
ref
|
|
.read(historyMetaStateNotifier.notifier)
|
|
.editHistoryRequest(historyModel!);
|
|
}
|
|
} else {
|
|
streamingMode = false;
|
|
}
|
|
|
|
if (!completer.isCompleted) {
|
|
completer.complete((response, duration, errorMessage));
|
|
}
|
|
}, onDone: () {
|
|
sub?.cancel();
|
|
state = {
|
|
...state!,
|
|
requestId: newRequestModel.copyWith(isStreaming: false),
|
|
};
|
|
unsave();
|
|
}, onError: (e) {
|
|
if (!completer.isCompleted) {
|
|
completer.complete((null, null, 'StreamError: $e'));
|
|
}
|
|
});
|
|
|
|
final (response, duration, errorMessage) = await completer.future;
|
|
|
|
if (response == null) {
|
|
newRequestModel = newRequestModel.copyWith(
|
|
responseStatus: -1,
|
|
message: errorMessage,
|
|
isWorking: false,
|
|
isStreaming: false,
|
|
);
|
|
} else {
|
|
final statusCode = response.statusCode;
|
|
httpResponseModel = baseHttpResponseModel.fromResponse(
|
|
response: response,
|
|
time: duration,
|
|
isStreamingResponse: isStreamingResponse,
|
|
);
|
|
|
|
//AI-FORMATTING for Non Streaming Varaint
|
|
if (!streamingMode &&
|
|
apiType == APIType.ai &&
|
|
response.statusCode == 200) {
|
|
final fb = executionRequestModel.aiRequestModel?.getFormattedOutput(
|
|
kJsonDecoder
|
|
.convert(httpResponseModel?.body ?? "Error parsing body"));
|
|
httpResponseModel = httpResponseModel?.copyWith(formattedBody: fb);
|
|
}
|
|
|
|
newRequestModel = newRequestModel.copyWith(
|
|
responseStatus: statusCode,
|
|
message: kResponseCodeReasons[statusCode],
|
|
httpResponseModel: httpResponseModel,
|
|
isWorking: false,
|
|
);
|
|
|
|
String newHistoryId = getNewUuid();
|
|
historyModel = HistoryRequestModel(
|
|
historyId: newHistoryId,
|
|
metaData: HistoryMetaModel(
|
|
historyId: newHistoryId,
|
|
requestId: requestId,
|
|
apiType: requestModel.apiType,
|
|
name: requestModel.name,
|
|
url: substitutedHttpRequestModel.url,
|
|
method: substitutedHttpRequestModel.method,
|
|
responseStatus: statusCode,
|
|
timeStamp: DateTime.now(),
|
|
),
|
|
httpRequestModel: substitutedHttpRequestModel,
|
|
aiRequestModel: executionRequestModel.aiRequestModel,
|
|
httpResponseModel: httpResponseModel!,
|
|
preRequestScript: requestModel.preRequestScript,
|
|
postRequestScript: requestModel.postRequestScript,
|
|
authModel: requestModel.httpRequestModel?.authModel,
|
|
);
|
|
|
|
ref
|
|
.read(historyMetaStateNotifier.notifier)
|
|
.addHistoryRequest(historyModel!);
|
|
|
|
if (!requestModel.postRequestScript.isNullOrEmpty()) {
|
|
newRequestModel = await handlePostResponseScript(
|
|
newRequestModel,
|
|
originalEnvironmentModel,
|
|
(envModel, updatedValues) {
|
|
ref
|
|
.read(environmentsStateNotifierProvider.notifier)
|
|
.updateEnvironment(
|
|
envModel.id,
|
|
name: envModel.name,
|
|
values: updatedValues,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Final state update
|
|
state = {
|
|
...state!,
|
|
requestId: newRequestModel,
|
|
};
|
|
|
|
unsave();
|
|
}
|
|
|
|
void cancelRequest() {
|
|
final id = ref.read(selectedIdStateProvider);
|
|
cancelHttpRequest(id);
|
|
unsave();
|
|
}
|
|
|
|
Future<void> clearData() async {
|
|
ref.read(clearDataStateProvider.notifier).state = true;
|
|
ref.read(selectedIdStateProvider.notifier).state = null;
|
|
await hiveHandler.clear();
|
|
ref.read(clearDataStateProvider.notifier).state = false;
|
|
ref.read(requestSequenceProvider.notifier).state = [];
|
|
state = {};
|
|
unsave();
|
|
}
|
|
|
|
bool loadData() {
|
|
var ids = hiveHandler.getIds();
|
|
if (ids == null || ids.length == 0) {
|
|
String newId = getNewUuid();
|
|
state = {
|
|
newId: RequestModel(
|
|
id: newId,
|
|
httpRequestModel: const HttpRequestModel(),
|
|
),
|
|
};
|
|
return true;
|
|
} else {
|
|
Map<String, RequestModel> data = {};
|
|
for (var id in ids) {
|
|
var jsonModel = hiveHandler.getRequestModel(id);
|
|
if (jsonModel != null) {
|
|
var jsonMap = Map<String, Object?>.from(jsonModel);
|
|
var requestModel = RequestModel.fromJson(jsonMap);
|
|
if (requestModel.httpRequestModel == null) {
|
|
requestModel = requestModel.copyWith(
|
|
httpRequestModel: const HttpRequestModel(),
|
|
);
|
|
}
|
|
data[id] = requestModel;
|
|
}
|
|
}
|
|
state = data;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
Future<void> saveData() async {
|
|
ref.read(saveDataStateProvider.notifier).state = true;
|
|
final saveResponse = ref.read(settingsProvider).saveResponses;
|
|
final ids = ref.read(requestSequenceProvider);
|
|
await hiveHandler.setIds(ids);
|
|
for (var id in ids) {
|
|
await hiveHandler.setRequestModel(
|
|
id,
|
|
saveResponse
|
|
? (state?[id])?.toJson()
|
|
: (state?[id]?.copyWith(httpResponseModel: null))?.toJson(),
|
|
);
|
|
}
|
|
|
|
await hiveHandler.removeUnused();
|
|
ref.read(saveDataStateProvider.notifier).state = false;
|
|
ref.read(hasUnsavedChangesProvider.notifier).state = false;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> exportDataToHAR() async {
|
|
var result = await collectionToHAR(state?.values.toList());
|
|
return result;
|
|
// return {
|
|
// "data": state!.map((e) => e.toJson(includeResponse: false)).toList()
|
|
// };
|
|
}
|
|
|
|
HttpRequestModel getSubstitutedHttpRequestModel(
|
|
HttpRequestModel httpRequestModel) {
|
|
var envMap = ref.read(availableEnvironmentVariablesStateProvider);
|
|
var activeEnvId = ref.read(activeEnvironmentIdStateProvider);
|
|
|
|
return substituteHttpRequestModel(
|
|
httpRequestModel,
|
|
envMap,
|
|
activeEnvId,
|
|
);
|
|
}
|
|
}
|