From fadf49372f150e7a1361724e8c8ad7b4c8604b3c Mon Sep 17 00:00:00 2001 From: Udhay-Adithya Date: Fri, 25 Apr 2025 23:26:31 +0530 Subject: [PATCH] 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 --- lib/models/request_model.dart | 2 + lib/models/request_model.freezed.dart | 81 +++++++++++--- lib/models/request_model.g.dart | 4 + lib/services/flutter_js_service.dart | 151 ++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 18 deletions(-) diff --git a/lib/models/request_model.dart b/lib/models/request_model.dart index bb9eefae..981ab9ae 100644 --- a/lib/models/request_model.dart +++ b/lib/models/request_model.dart @@ -22,6 +22,8 @@ class RequestModel with _$RequestModel { HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) @Default(false) bool isWorking, @JsonKey(includeToJson: false) DateTime? sendingTime, + @Default("") String preRequestScript, + @Default("") String postRequestScript, }) = _RequestModel; factory RequestModel.fromJson(Map json) => diff --git a/lib/models/request_model.freezed.dart b/lib/models/request_model.freezed.dart index b237e372..2a830614 100644 --- a/lib/models/request_model.freezed.dart +++ b/lib/models/request_model.freezed.dart @@ -35,6 +35,8 @@ mixin _$RequestModel { bool get isWorking => throw _privateConstructorUsedError; @JsonKey(includeToJson: false) DateTime? get sendingTime => throw _privateConstructorUsedError; + String get preRequestScript => throw _privateConstructorUsedError; + String get postRequestScript => throw _privateConstructorUsedError; /// Serializes this RequestModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -63,7 +65,9 @@ abstract class $RequestModelCopyWith<$Res> { String? message, HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, - @JsonKey(includeToJson: false) DateTime? sendingTime}); + @JsonKey(includeToJson: false) DateTime? sendingTime, + String preRequestScript, + String postRequestScript}); $HttpRequestModelCopyWith<$Res>? get httpRequestModel; $HttpResponseModelCopyWith<$Res>? get httpResponseModel; @@ -95,6 +99,8 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? preRequestScript = null, + Object? postRequestScript = null, }) { return _then(_value.copyWith( id: null == id @@ -141,6 +147,14 @@ class _$RequestModelCopyWithImpl<$Res, $Val extends RequestModel> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable 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); } @@ -192,7 +206,9 @@ abstract class _$$RequestModelImplCopyWith<$Res> String? message, HttpResponseModel? httpResponseModel, @JsonKey(includeToJson: false) bool isWorking, - @JsonKey(includeToJson: false) DateTime? sendingTime}); + @JsonKey(includeToJson: false) DateTime? sendingTime, + String preRequestScript, + String postRequestScript}); @override $HttpRequestModelCopyWith<$Res>? get httpRequestModel; @@ -224,6 +240,8 @@ class __$$RequestModelImplCopyWithImpl<$Res> Object? httpResponseModel = freezed, Object? isWorking = null, Object? sendingTime = freezed, + Object? preRequestScript = null, + Object? postRequestScript = null, }) { return _then(_$RequestModelImpl( id: null == id @@ -269,6 +287,14 @@ class __$$RequestModelImplCopyWithImpl<$Res> ? _value.sendingTime : sendingTime // ignore: cast_nullable_to_non_nullable 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.httpResponseModel, @JsonKey(includeToJson: false) this.isWorking = false, - @JsonKey(includeToJson: false) this.sendingTime}); + @JsonKey(includeToJson: false) this.sendingTime, + this.preRequestScript = "", + this.postRequestScript = ""}); factory _$RequestModelImpl.fromJson(Map json) => _$$RequestModelImplFromJson(json); @@ -321,10 +349,16 @@ class _$RequestModelImpl implements _RequestModel { @override @JsonKey(includeToJson: false) final DateTime? sendingTime; + @override + @JsonKey() + final String preRequestScript; + @override + @JsonKey() + final String postRequestScript; @override 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 @@ -349,7 +383,11 @@ class _$RequestModelImpl implements _RequestModel { (identical(other.isWorking, isWorking) || other.isWorking == isWorking) && (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) @@ -366,7 +404,9 @@ class _$RequestModelImpl implements _RequestModel { message, httpResponseModel, isWorking, - sendingTime); + sendingTime, + preRequestScript, + postRequestScript); /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. @@ -386,18 +426,19 @@ class _$RequestModelImpl implements _RequestModel { abstract class _RequestModel implements RequestModel { const factory _RequestModel( - {required final String id, - final APIType apiType, - final String name, - final String description, - @JsonKey(includeToJson: false) final dynamic requestTabIndex, - final HttpRequestModel? httpRequestModel, - final int? responseStatus, - final String? message, - final HttpResponseModel? httpResponseModel, - @JsonKey(includeToJson: false) final bool isWorking, - @JsonKey(includeToJson: false) final DateTime? sendingTime}) = - _$RequestModelImpl; + {required final String id, + final APIType apiType, + final String name, + final String description, + @JsonKey(includeToJson: false) final dynamic requestTabIndex, + final HttpRequestModel? httpRequestModel, + final int? responseStatus, + final String? message, + final HttpResponseModel? httpResponseModel, + @JsonKey(includeToJson: false) final bool isWorking, + @JsonKey(includeToJson: false) final DateTime? sendingTime, + final String preRequestScript, + final String postRequestScript}) = _$RequestModelImpl; factory _RequestModel.fromJson(Map json) = _$RequestModelImpl.fromJson; @@ -427,6 +468,10 @@ abstract class _RequestModel implements RequestModel { @override @JsonKey(includeToJson: false) DateTime? get sendingTime; + @override + String get preRequestScript; + @override + String get postRequestScript; /// Create a copy of RequestModel /// with the given fields replaced by the non-null parameter values. diff --git a/lib/models/request_model.g.dart b/lib/models/request_model.g.dart index 68b56395..33b17910 100644 --- a/lib/models/request_model.g.dart +++ b/lib/models/request_model.g.dart @@ -27,6 +27,8 @@ _$RequestModelImpl _$$RequestModelImplFromJson(Map json) => _$RequestModelImpl( sendingTime: json['sendingTime'] == null ? null : DateTime.parse(json['sendingTime'] as String), + preRequestScript: json['preRequestScript'] as String? ?? "", + postRequestScript: json['postRequestScript'] as String? ?? "", ); Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => @@ -39,6 +41,8 @@ Map _$$RequestModelImplToJson(_$RequestModelImpl instance) => 'responseStatus': instance.responseStatus, 'message': instance.message, 'httpResponseModel': instance.httpResponseModel?.toJson(), + 'preRequestScript': instance.preRequestScript, + 'postRequestScript': instance.postRequestScript, }; const _$APITypeEnumMap = { diff --git a/lib/services/flutter_js_service.dart b/lib/services/flutter_js_service.dart index 2cb79dce..17aedc88 100644 --- a/lib/services/flutter_js_service.dart +++ b/lib/services/flutter_js_service.dart @@ -1,5 +1,10 @@ +// ignore_for_file: avoid_print + +import 'dart:convert'; 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_js/flutter_js.dart'; @@ -21,3 +26,149 @@ void evaluate(String code) { 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 updatedEnvironment + })> executePreRequestScript({ + required RequestModel currentRequestModel, + required Map 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 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) { + // Deserialize Request + if (resultMap.containsKey('request') && resultMap['request'] is Map) { + try { + resultingRequest = HttpRequestModel.fromJson( + Map.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.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 +}