mirror of
https://github.com/foss42/apidash.git
synced 2025-11-30 09:48:58 +08:00
651 lines
20 KiB
Dart
651 lines
20 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 'package:apidash/terminal/terminal.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) {
|
|
final defaultModel = ref.read(settingsProvider).defaultAIModel;
|
|
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: defaultModel == null
|
|
? const AIRequestModel()
|
|
: AIRequestModel.fromJson(defaultModel)),
|
|
};
|
|
} 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 ref
|
|
.read(jsRuntimeNotifierProvider.notifier)
|
|
.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!);
|
|
}
|
|
|
|
// Terminal
|
|
final terminal = ref.read(terminalStateProvider.notifier);
|
|
|
|
var valRes = getValidationResult(substitutedHttpRequestModel);
|
|
if (valRes != null) {
|
|
terminal.logSystem(
|
|
category: 'validation',
|
|
message: valRes,
|
|
level: TerminalLevel.error,
|
|
);
|
|
ref.read(showTerminalBadgeProvider.notifier).state = true;
|
|
}
|
|
|
|
// Terminal: start network log
|
|
final logId = terminal.startNetwork(
|
|
apiType: executionRequestModel.apiType,
|
|
method: substitutedHttpRequestModel.method,
|
|
url: substitutedHttpRequestModel.url,
|
|
requestId: requestId,
|
|
requestHeaders: substitutedHttpRequestModel.enabledHeadersMap,
|
|
requestBodyPreview: substitutedHttpRequestModel.body,
|
|
isStreaming: true,
|
|
);
|
|
|
|
// 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,
|
|
};
|
|
// Terminal: append chunk preview
|
|
if (response != null && response.body.isNotEmpty) {
|
|
terminal.addNetworkChunk(
|
|
logId,
|
|
BodyChunk(
|
|
ts: DateTime.now(),
|
|
text: response.body,
|
|
sizeBytes: response.body.codeUnits.length,
|
|
),
|
|
);
|
|
}
|
|
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'));
|
|
}
|
|
terminal.failNetwork(logId, 'StreamError: $e');
|
|
});
|
|
|
|
final (response, duration, errorMessage) = await completer.future;
|
|
|
|
if (response == null) {
|
|
newRequestModel = newRequestModel.copyWith(
|
|
responseStatus: -1,
|
|
message: errorMessage,
|
|
isWorking: false,
|
|
isStreaming: false,
|
|
);
|
|
terminal.failNetwork(logId, errorMessage ?? 'Unknown error');
|
|
} 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,
|
|
);
|
|
|
|
terminal.completeNetwork(
|
|
logId,
|
|
statusCode: statusCode,
|
|
responseHeaders: response.headers,
|
|
responseBodyPreview: httpResponseModel?.body,
|
|
duration: duration,
|
|
);
|
|
|
|
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 ref
|
|
.read(jsRuntimeNotifierProvider.notifier)
|
|
.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,
|
|
);
|
|
}
|
|
}
|