mirror of
https://github.com/foss42/apidash.git
synced 2025-12-02 18:57:05 +08:00
feat: add support for pre-request scripts
Introduces `preRequestScript` and `postRequestScript` fields to the request model to store user-defined scripts. Implements a service using `flutter_js` to execute JavaScript pre-request scripts before a request is sent. The script can access and modify request data (like headers, body, URL) and environment variables. Adds bridging to forward JavaScript `console.log`, `console.warn`, and `console.error` calls to the Dart console for easier debugging
This commit is contained in:
@@ -22,6 +22,8 @@ class RequestModel with _$RequestModel {
|
|||||||
HttpResponseModel? httpResponseModel,
|
HttpResponseModel? httpResponseModel,
|
||||||
@JsonKey(includeToJson: false) @Default(false) bool isWorking,
|
@JsonKey(includeToJson: false) @Default(false) bool isWorking,
|
||||||
@JsonKey(includeToJson: false) DateTime? sendingTime,
|
@JsonKey(includeToJson: false) DateTime? sendingTime,
|
||||||
|
@Default("") String preRequestScript,
|
||||||
|
@Default("") String postRequestScript,
|
||||||
}) = _RequestModel;
|
}) = _RequestModel;
|
||||||
|
|
||||||
factory RequestModel.fromJson(Map<String, Object?> json) =>
|
factory RequestModel.fromJson(Map<String, Object?> json) =>
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ mixin _$RequestModel {
|
|||||||
bool get isWorking => throw _privateConstructorUsedError;
|
bool get isWorking => throw _privateConstructorUsedError;
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
DateTime? get sendingTime => throw _privateConstructorUsedError;
|
DateTime? get sendingTime => throw _privateConstructorUsedError;
|
||||||
|
String get preRequestScript => throw _privateConstructorUsedError;
|
||||||
|
String get postRequestScript => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
/// Serializes this RequestModel to a JSON map.
|
/// Serializes this RequestModel to a JSON map.
|
||||||
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
|
||||||
@@ -63,7 +65,9 @@ abstract class $RequestModelCopyWith<$Res> {
|
|||||||
String? message,
|
String? message,
|
||||||
HttpResponseModel? httpResponseModel,
|
HttpResponseModel? httpResponseModel,
|
||||||
@JsonKey(includeToJson: false) bool isWorking,
|
@JsonKey(includeToJson: false) bool isWorking,
|
||||||
@JsonKey(includeToJson: false) DateTime? sendingTime});
|
@JsonKey(includeToJson: false) DateTime? sendingTime,
|
||||||
|
String preRequestScript,
|
||||||
|
String postRequestScript});
|
||||||
|
|
||||||
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
|
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
|
||||||
$HttpResponseModelCopyWith<$Res>? get httpResponseModel;
|
$HttpResponseModelCopyWith<$Res>? get httpResponseModel;
|
||||||
@@ -95,6 +99,8 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel>
|
|||||||
Object? httpResponseModel = freezed,
|
Object? httpResponseModel = freezed,
|
||||||
Object? isWorking = null,
|
Object? isWorking = null,
|
||||||
Object? sendingTime = freezed,
|
Object? sendingTime = freezed,
|
||||||
|
Object? preRequestScript = null,
|
||||||
|
Object? postRequestScript = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -141,6 +147,14 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel>
|
|||||||
? _value.sendingTime
|
? _value.sendingTime
|
||||||
: sendingTime // ignore: cast_nullable_to_non_nullable
|
: sendingTime // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
|
preRequestScript: null == preRequestScript
|
||||||
|
? _value.preRequestScript
|
||||||
|
: preRequestScript // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
postRequestScript: null == postRequestScript
|
||||||
|
? _value.postRequestScript
|
||||||
|
: postRequestScript // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
) as $Val);
|
) as $Val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +206,9 @@ abstract class _$$RequestModelImplCopyWith<$Res>
|
|||||||
String? message,
|
String? message,
|
||||||
HttpResponseModel? httpResponseModel,
|
HttpResponseModel? httpResponseModel,
|
||||||
@JsonKey(includeToJson: false) bool isWorking,
|
@JsonKey(includeToJson: false) bool isWorking,
|
||||||
@JsonKey(includeToJson: false) DateTime? sendingTime});
|
@JsonKey(includeToJson: false) DateTime? sendingTime,
|
||||||
|
String preRequestScript,
|
||||||
|
String postRequestScript});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
|
$HttpRequestModelCopyWith<$Res>? get httpRequestModel;
|
||||||
@@ -224,6 +240,8 @@ class __$$RequestModelImplCopyWithImpl<$Res>
|
|||||||
Object? httpResponseModel = freezed,
|
Object? httpResponseModel = freezed,
|
||||||
Object? isWorking = null,
|
Object? isWorking = null,
|
||||||
Object? sendingTime = freezed,
|
Object? sendingTime = freezed,
|
||||||
|
Object? preRequestScript = null,
|
||||||
|
Object? postRequestScript = null,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$RequestModelImpl(
|
return _then(_$RequestModelImpl(
|
||||||
id: null == id
|
id: null == id
|
||||||
@@ -269,6 +287,14 @@ class __$$RequestModelImplCopyWithImpl<$Res>
|
|||||||
? _value.sendingTime
|
? _value.sendingTime
|
||||||
: sendingTime // ignore: cast_nullable_to_non_nullable
|
: sendingTime // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
|
preRequestScript: null == preRequestScript
|
||||||
|
? _value.preRequestScript
|
||||||
|
: preRequestScript // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
|
postRequestScript: null == postRequestScript
|
||||||
|
? _value.postRequestScript
|
||||||
|
: postRequestScript // ignore: cast_nullable_to_non_nullable
|
||||||
|
as String,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -288,7 +314,9 @@ class _$RequestModelImpl implements _RequestModel {
|
|||||||
this.message,
|
this.message,
|
||||||
this.httpResponseModel,
|
this.httpResponseModel,
|
||||||
@JsonKey(includeToJson: false) this.isWorking = false,
|
@JsonKey(includeToJson: false) this.isWorking = false,
|
||||||
@JsonKey(includeToJson: false) this.sendingTime});
|
@JsonKey(includeToJson: false) this.sendingTime,
|
||||||
|
this.preRequestScript = "",
|
||||||
|
this.postRequestScript = ""});
|
||||||
|
|
||||||
factory _$RequestModelImpl.fromJson(Map<String, dynamic> json) =>
|
factory _$RequestModelImpl.fromJson(Map<String, dynamic> json) =>
|
||||||
_$$RequestModelImplFromJson(json);
|
_$$RequestModelImplFromJson(json);
|
||||||
@@ -321,10 +349,16 @@ class _$RequestModelImpl implements _RequestModel {
|
|||||||
@override
|
@override
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
final DateTime? sendingTime;
|
final DateTime? sendingTime;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final String preRequestScript;
|
||||||
|
@override
|
||||||
|
@JsonKey()
|
||||||
|
final String postRequestScript;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime)';
|
return 'RequestModel(id: $id, apiType: $apiType, name: $name, description: $description, requestTabIndex: $requestTabIndex, httpRequestModel: $httpRequestModel, responseStatus: $responseStatus, message: $message, httpResponseModel: $httpResponseModel, isWorking: $isWorking, sendingTime: $sendingTime, preRequestScript: $preRequestScript, postRequestScript: $postRequestScript)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -349,7 +383,11 @@ class _$RequestModelImpl implements _RequestModel {
|
|||||||
(identical(other.isWorking, isWorking) ||
|
(identical(other.isWorking, isWorking) ||
|
||||||
other.isWorking == isWorking) &&
|
other.isWorking == isWorking) &&
|
||||||
(identical(other.sendingTime, sendingTime) ||
|
(identical(other.sendingTime, sendingTime) ||
|
||||||
other.sendingTime == sendingTime));
|
other.sendingTime == sendingTime) &&
|
||||||
|
(identical(other.preRequestScript, preRequestScript) ||
|
||||||
|
other.preRequestScript == preRequestScript) &&
|
||||||
|
(identical(other.postRequestScript, postRequestScript) ||
|
||||||
|
other.postRequestScript == postRequestScript));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
@@ -366,7 +404,9 @@ class _$RequestModelImpl implements _RequestModel {
|
|||||||
message,
|
message,
|
||||||
httpResponseModel,
|
httpResponseModel,
|
||||||
isWorking,
|
isWorking,
|
||||||
sendingTime);
|
sendingTime,
|
||||||
|
preRequestScript,
|
||||||
|
postRequestScript);
|
||||||
|
|
||||||
/// Create a copy of RequestModel
|
/// Create a copy of RequestModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@@ -386,18 +426,19 @@ class _$RequestModelImpl implements _RequestModel {
|
|||||||
|
|
||||||
abstract class _RequestModel implements RequestModel {
|
abstract class _RequestModel implements RequestModel {
|
||||||
const factory _RequestModel(
|
const factory _RequestModel(
|
||||||
{required final String id,
|
{required final String id,
|
||||||
final APIType apiType,
|
final APIType apiType,
|
||||||
final String name,
|
final String name,
|
||||||
final String description,
|
final String description,
|
||||||
@JsonKey(includeToJson: false) final dynamic requestTabIndex,
|
@JsonKey(includeToJson: false) final dynamic requestTabIndex,
|
||||||
final HttpRequestModel? httpRequestModel,
|
final HttpRequestModel? httpRequestModel,
|
||||||
final int? responseStatus,
|
final int? responseStatus,
|
||||||
final String? message,
|
final String? message,
|
||||||
final HttpResponseModel? httpResponseModel,
|
final HttpResponseModel? httpResponseModel,
|
||||||
@JsonKey(includeToJson: false) final bool isWorking,
|
@JsonKey(includeToJson: false) final bool isWorking,
|
||||||
@JsonKey(includeToJson: false) final DateTime? sendingTime}) =
|
@JsonKey(includeToJson: false) final DateTime? sendingTime,
|
||||||
_$RequestModelImpl;
|
final String preRequestScript,
|
||||||
|
final String postRequestScript}) = _$RequestModelImpl;
|
||||||
|
|
||||||
factory _RequestModel.fromJson(Map<String, dynamic> json) =
|
factory _RequestModel.fromJson(Map<String, dynamic> json) =
|
||||||
_$RequestModelImpl.fromJson;
|
_$RequestModelImpl.fromJson;
|
||||||
@@ -427,6 +468,10 @@ abstract class _RequestModel implements RequestModel {
|
|||||||
@override
|
@override
|
||||||
@JsonKey(includeToJson: false)
|
@JsonKey(includeToJson: false)
|
||||||
DateTime? get sendingTime;
|
DateTime? get sendingTime;
|
||||||
|
@override
|
||||||
|
String get preRequestScript;
|
||||||
|
@override
|
||||||
|
String get postRequestScript;
|
||||||
|
|
||||||
/// Create a copy of RequestModel
|
/// Create a copy of RequestModel
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl(
|
|||||||
sendingTime: json['sendingTime'] == null
|
sendingTime: json['sendingTime'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['sendingTime'] as String),
|
: DateTime.parse(json['sendingTime'] as String),
|
||||||
|
preRequestScript: json['preRequestScript'] as String? ?? "",
|
||||||
|
postRequestScript: json['postRequestScript'] as String? ?? "",
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$$RequestModelImplToJson(_$RequestModelImpl instance) =>
|
Map<String, dynamic> _$$RequestModelImplToJson(_$RequestModelImpl instance) =>
|
||||||
@@ -39,6 +41,8 @@ Map<String, dynamic> _$$RequestModelImplToJson(_$RequestModelImpl instance) =>
|
|||||||
'responseStatus': instance.responseStatus,
|
'responseStatus': instance.responseStatus,
|
||||||
'message': instance.message,
|
'message': instance.message,
|
||||||
'httpResponseModel': instance.httpResponseModel?.toJson(),
|
'httpResponseModel': instance.httpResponseModel?.toJson(),
|
||||||
|
'preRequestScript': instance.preRequestScript,
|
||||||
|
'postRequestScript': instance.postRequestScript,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$APITypeEnumMap = {
|
const _$APITypeEnumMap = {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
|
// ignore_for_file: avoid_print
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
|
|
||||||
|
import 'package:apidash/models/request_model.dart';
|
||||||
|
import 'package:apidash_core/models/http_request_model.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_js/flutter_js.dart';
|
import 'package:flutter_js/flutter_js.dart';
|
||||||
|
|
||||||
@@ -21,3 +26,149 @@ void evaluate(String code) {
|
|||||||
log('ERROR: ${e.details}');
|
log('ERROR: ${e.details}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: These log statements can be printed in a custom api dash terminal.
|
||||||
|
void setupJsBridge() {
|
||||||
|
jsRuntime.onMessage('consoleLog', (args) {
|
||||||
|
try {
|
||||||
|
final decodedArgs = jsonDecode(args as String);
|
||||||
|
if (decodedArgs is List) {
|
||||||
|
print('[JS LOG]: ${decodedArgs.map((e) => e.toString()).join(' ')}');
|
||||||
|
} else {
|
||||||
|
print('[JS LOG]: $decodedArgs');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[JS LOG ERROR decoding]: $args, Error: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jsRuntime.onMessage('consoleWarn', (args) {
|
||||||
|
try {
|
||||||
|
final decodedArgs = jsonDecode(args as String);
|
||||||
|
if (decodedArgs is List) {
|
||||||
|
print('[JS WARN]: ${decodedArgs.map((e) => e.toString()).join(' ')}');
|
||||||
|
} else {
|
||||||
|
print('[JS WARN]: $decodedArgs');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[JS WARN ERROR decoding]: $args, Error: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jsRuntime.onMessage('consoleError', (args) {
|
||||||
|
try {
|
||||||
|
final decodedArgs = jsonDecode(args as String);
|
||||||
|
if (decodedArgs is List) {
|
||||||
|
print('[JS ERROR]: ${decodedArgs.map((e) => e.toString()).join(' ')}');
|
||||||
|
} else {
|
||||||
|
print('[JS ERROR]: $decodedArgs');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('[JS ERROR ERROR decoding]: $args, Error: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
jsRuntime.onMessage('fatalError', (args) {
|
||||||
|
try {
|
||||||
|
final errorDetails = jsonDecode(args as String);
|
||||||
|
print('[JS FATAL ERROR]: ${errorDetails['message']}');
|
||||||
|
if (errorDetails['error']) print(' Error: ${errorDetails['error']}');
|
||||||
|
if (errorDetails['stack']) print(' Stack: ${errorDetails['stack']}');
|
||||||
|
} catch (e) {
|
||||||
|
print('[JS FATAL ERROR decoding error]: $args, Error: $e');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//TODO: Add handlers for 'testResult'
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<
|
||||||
|
({
|
||||||
|
HttpRequestModel updatedRequest,
|
||||||
|
Map<String, dynamic> updatedEnvironment
|
||||||
|
})> executePreRequestScript({
|
||||||
|
required RequestModel currentRequestModel,
|
||||||
|
required Map<String, dynamic> activeEnvironment,
|
||||||
|
required String setupScript,
|
||||||
|
}) async {
|
||||||
|
if (currentRequestModel.preRequestScript.trim().isEmpty) {
|
||||||
|
// No script, return original data
|
||||||
|
// return (
|
||||||
|
// updatedRequest: currentRequestModel.httpRequestModel,
|
||||||
|
// updatedEnvironment: activeEnvironment
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
final httpRequest = currentRequestModel.httpRequestModel;
|
||||||
|
final userScript = currentRequestModel.preRequestScript;
|
||||||
|
|
||||||
|
// Prepare Data
|
||||||
|
final requestJson = jsonEncode(httpRequest?.toJson());
|
||||||
|
final environmentJson = jsonEncode(activeEnvironment);
|
||||||
|
|
||||||
|
// Inject data as JS variables
|
||||||
|
// Escape strings properly if embedding directly
|
||||||
|
final dataInjection = """
|
||||||
|
const injectedRequestJson = ${jsEscapeString(requestJson)};
|
||||||
|
const injectedEnvironmentJson = ${jsEscapeString(environmentJson)};
|
||||||
|
const injectedResponseJson = null; // Not needed for pre-request
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Concatenate & Add Return
|
||||||
|
final fullScript = """
|
||||||
|
$dataInjection
|
||||||
|
$setupScript
|
||||||
|
// --- User Script ---
|
||||||
|
$userScript
|
||||||
|
// --- Return Result ---
|
||||||
|
JSON.stringify({ request: request, environment: environment });
|
||||||
|
""";
|
||||||
|
// TODO: Do something better to avoid null check here.
|
||||||
|
HttpRequestModel resultingRequest = httpRequest!;
|
||||||
|
Map<String, dynamic> resultingEnvironment = Map.from(activeEnvironment);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute
|
||||||
|
final JsEvalResult result = await jsRuntime.evaluateAsync(fullScript);
|
||||||
|
|
||||||
|
// Process Results
|
||||||
|
if (result.isError) {
|
||||||
|
print("Pre-request script execution error: ${result.stringResult}");
|
||||||
|
// Handle error - maybe show in UI, keep original request/env
|
||||||
|
} else if (result.stringResult.isNotEmpty) {
|
||||||
|
final resultMap = jsonDecode(result.stringResult);
|
||||||
|
if (resultMap is Map<String, dynamic>) {
|
||||||
|
// Deserialize Request
|
||||||
|
if (resultMap.containsKey('request') && resultMap['request'] is Map) {
|
||||||
|
try {
|
||||||
|
resultingRequest = HttpRequestModel.fromJson(
|
||||||
|
Map<String, Object?>.from(resultMap['request']));
|
||||||
|
} catch (e) {
|
||||||
|
print("Error deserializing modified request from script: $e");
|
||||||
|
//TODO: Handle error - maybe keep original request?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Get Environment Modifications
|
||||||
|
if (resultMap.containsKey('environment') &&
|
||||||
|
resultMap['environment'] is Map) {
|
||||||
|
resultingEnvironment =
|
||||||
|
Map<String, dynamic>.from(resultMap['environment']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Dart-level error during pre-request script execution: $e");
|
||||||
|
// Handle Dart-level errors (e.g., JS runtime issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
updatedRequest: resultingRequest,
|
||||||
|
updatedEnvironment: resultingEnvironment
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to properly escape strings for JS embedding
|
||||||
|
String jsEscapeString(String s) {
|
||||||
|
return jsonEncode(
|
||||||
|
s); // jsonEncode handles escaping correctly for JS string literals
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user