feat: allow scripts to modify environment variables

Refactors pre-request script execution to support environment variable manipulation.

Introduces a simplified key-value map for the environment within the JavaScript context, allowing easier script interaction. Updates the `ad.environment` JS API accordingly.

Implements logic on the Dart side to translate the structured environment model to the simple map before script execution and merge changes back from the map to the model afterward, persisting the modifications.
This commit is contained in:
Udhay-Adithya
2025-05-01 20:14:00 +05:30
parent 71c6818093
commit 0645ab1a33
2 changed files with 112 additions and 29 deletions

View File

@@ -496,12 +496,12 @@ const String setupScript = r"""
let request = {}; // Will hold the RequestModel data let request = {}; // Will hold the RequestModel data
let response = {}; // Will hold the ResponseModel data (only for post-request) let response = {}; // Will hold the ResponseModel data (only for post-request)
let environment = {}; // Will hold the active environment variables let environment = {}; // Will hold the *active* environment variables as a simple {key: value} map
// Note: Using 'let' because environment might be completely cleared/reassigned. // Note: Using 'let' because environment might be completely cleared/reassigned by ad.environment.clear().
try { try {
// 'injectedRequestJson' should always be provided (even if empty for some edge case) // 'injectedRequestJson' should always be provided
if (typeof injectedRequestJson !== 'undefined' && injectedRequestJson) { if (typeof injectedRequestJson !== 'undefined' && injectedRequestJson) {
request = JSON.parse(injectedRequestJson); request = JSON.parse(injectedRequestJson);
// Ensure essential arrays exist if they are null/undefined after parsing // Ensure essential arrays exist if they are null/undefined after parsing
@@ -509,7 +509,6 @@ try {
request.params = request.params || []; request.params = request.params || [];
request.formData = request.formData || []; request.formData = request.formData || [];
} else { } else {
// Should not happen based on the plan, but good to log
sendMessage('consoleError', JSON.stringify(['Setup Error: injectedRequestJson is missing or empty.'])); sendMessage('consoleError', JSON.stringify(['Setup Error: injectedRequestJson is missing or empty.']));
} }
@@ -523,9 +522,29 @@ try {
// 'injectedEnvironmentJson' should always be provided // 'injectedEnvironmentJson' should always be provided
if (typeof injectedEnvironmentJson !== 'undefined' && injectedEnvironmentJson) { if (typeof injectedEnvironmentJson !== 'undefined' && injectedEnvironmentJson) {
environment = JSON.parse(injectedEnvironmentJson); const parsedEnvData = JSON.parse(injectedEnvironmentJson);
environment = {}; // Initialize the target simple map
if (parsedEnvData && Array.isArray(parsedEnvData.values)) {
parsedEnvData.values.forEach(variable => {
// Check if the variable object is valid and enabled
if (variable && typeof variable === 'object' && variable.enabled === true && typeof variable.key === 'string') {
// Add the key-value pair to our simplified 'environment' map
environment[variable.key] = variable.value;
}
});
// sendMessage('consoleLog', JSON.stringify(['Successfully parsed environment variables.']));
} else {
// Log a warning if the structure is not as expected, but continue with an empty env
sendMessage('consoleWarn', JSON.stringify([
'Setup Warning: injectedEnvironmentJson does not have the expected structure ({values: Array}). Using an empty environment.',
'Received Data:', parsedEnvData // Log received data for debugging
]));
environment = {}; // Ensure it's an empty object
}
} else { } else {
// Should not happen based on the plan, but good to log
sendMessage('consoleError', JSON.stringify(['Setup Error: injectedEnvironmentJson is missing or empty.'])); sendMessage('consoleError', JSON.stringify(['Setup Error: injectedEnvironmentJson is missing or empty.']));
environment = {}; // Initialize to empty object to avoid errors later environment = {}; // Initialize to empty object to avoid errors later
} }
@@ -546,7 +565,7 @@ try {
// This object provides functions to interact with the request, response, // This object provides functions to interact with the request, response,
// environment, and the Dart host application. // environment, and the Dart host application.
var ad = { const ad = {
/** /**
* Functions to modify the request object *before* it is sent. * Functions to modify the request object *before* it is sent.
* Only available in pre-request scripts. * Only available in pre-request scripts.
@@ -717,7 +736,7 @@ var ad = {
* @param {string} [contentType] Optionally specify the Content-Type (e.g., 'application/json', 'text/plain'). If not set, defaults to 'text/plain' or 'application/json' if newBody is an object. * @param {string} [contentType] Optionally specify the Content-Type (e.g., 'application/json', 'text/plain'). If not set, defaults to 'text/plain' or 'application/json' if newBody is an object.
*/ */
set: (newBody, contentType) => { set: (newBody, contentType) => {
if (!request || typeof request === 'object') return; // Safety check if (!request || typeof request !== 'object') return; // Safety check fix: check !request or typeof !== object
let finalBody = ''; let finalBody = '';
let finalContentType = contentType; let finalContentType = contentType;
@@ -839,11 +858,12 @@ var ad = {
* @returns {object|undefined} The parsed JSON object, or undefined if parsing fails or body is empty. * @returns {object|undefined} The parsed JSON object, or undefined if parsing fails or body is empty.
*/ */
json: () => { json: () => {
if (!ad.response.body) { const bodyContent = ad.response.body; // Assign to variable first
if (!bodyContent) { // Check the variable
return undefined; return undefined;
} }
try { try {
return JSON.parse(ad.response.body); return JSON.parse(bodyContent); // Parse the variable
} catch (e) { } catch (e) {
ad.console.error("Failed to parse response body as JSON:", e.toString()); ad.console.error("Failed to parse response body as JSON:", e.toString());
return undefined; return undefined;
@@ -859,61 +879,70 @@ var ad = {
const headers = ad.response.headers; const headers = ad.response.headers;
if (!headers || typeof key !== 'string') return undefined; if (!headers || typeof key !== 'string') return undefined;
const lowerKey = key.toLowerCase(); const lowerKey = key.toLowerCase();
// Find the key in the headers object case-insensitively
const headerKey = Object.keys(headers).find(k => k.toLowerCase() === lowerKey); const headerKey = Object.keys(headers).find(k => k.toLowerCase() === lowerKey);
return headerKey ? headers[headerKey] : undefined; return headerKey ? headers[headerKey] : undefined; // Return the value using the found key
} }
}, },
/** /**
* Access and modify environment variables for the active environment. * Access and modify environment variables for the active environment.
* Changes are made to the 'environment' JS object and sent back to Dart. * Changes are made to the 'environment' JS object (simple {key: value} map)
* and sent back to Dart. Dart side will need to merge these changes back
* into the original structured format if needed.
*/ */
environment: { environment: {
/** /**
* Gets the value of an environment variable. * Gets the value of an environment variable from the simplified map.
* @param {string} key The variable name. * @param {string} key The variable name.
* @returns {any} The variable value or undefined if not found. * @returns {any} The variable value or undefined if not found.
*/ */
get: (key) => { get: (key) => {
// Access the simplified 'environment' object directly
return (environment && typeof environment === 'object') ? environment[key] : undefined; return (environment && typeof environment === 'object') ? environment[key] : undefined;
}, },
/** /**
* Sets the value of an environment variable. * Sets the value of an environment variable in the simplified map.
* @param {string} key The variable name. * @param {string} key The variable name.
* @param {any} value The variable value. Should be JSON-serializable (string, number, boolean, object, array). * @param {any} value The variable value. Should be JSON-serializable (string, number, boolean, object, array).
*/ */
set: (key, value) => { set: (key, value) => {
if (environment && typeof environment === 'object' && typeof key === 'string') { if (environment && typeof environment === 'object' && typeof key === 'string') {
// Modify the simplified 'environment' object
environment[key] = value; environment[key] = value;
} }
}, },
/** /**
* Removes an environment variable. * Removes an environment variable from the simplified map.
* @param {string} key The variable name to remove. * @param {string} key The variable name to remove.
*/ */
unset: (key) => { unset: (key) => {
if (environment && typeof environment === 'object') { if (environment && typeof environment === 'object') {
// Modify the simplified 'environment' object
delete environment[key]; delete environment[key];
} }
}, },
/** /**
* Checks if an environment variable exists. * Checks if an environment variable exists in the simplified map.
* @param {string} key The variable name. * @param {string} key The variable name.
* @returns {boolean} True if the variable exists, false otherwise. * @returns {boolean} True if the variable exists, false otherwise.
*/ */
has: (key) => { has: (key) => {
// Check the simplified 'environment' object
return (environment && typeof environment === 'object') ? environment.hasOwnProperty(key) : false; return (environment && typeof environment === 'object') ? environment.hasOwnProperty(key) : false;
}, },
/** /**
* Removes all variables from the current environment scope. * Removes all variables from the simplified environment map scope.
*/ */
clear: () => { clear: () => {
if (environment && typeof environment === 'object') { if (environment && typeof environment === 'object') {
// Clear the simplified 'environment' object
for (const key in environment) { for (const key in environment) {
if (environment.hasOwnProperty(key)) { if (environment.hasOwnProperty(key)) {
delete environment[key]; delete environment[key];
} }
} }
// Alternatively, just reassign: environment = {};
} }
} }
// Note: A separate 'globals' object could be added here if global variables are implemented distinctly. // Note: A separate 'globals' object could be added here if global variables are implemented distinctly.

View File

@@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:apidash/services/flutter_js_service.dart'; import 'package:apidash/services/flutter_js_service.dart';
import 'package:apidash_core/apidash_core.dart'; import 'package:apidash_core/apidash_core.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -269,10 +267,74 @@ class CollectionStateNotifier
unsave(); unsave();
} }
Future<void> handlePreRequestScript(
RequestModel requestModel,
EnvironmentModel? originalEnvironmentModel,
) async {
final scriptResult = await executePreRequestScript(
currentRequestModel: requestModel,
activeEnvironment: originalEnvironmentModel?.toJson() ?? {},
);
requestModel =
requestModel.copyWith(httpRequestModel: scriptResult.updatedRequest);
if (originalEnvironmentModel != null) {
final updatedEnvironmentMap = scriptResult.updatedEnvironment;
final List<EnvironmentVariableModel> newValues = [];
final Map<String, dynamic> mutableUpdatedEnv =
Map.from(updatedEnvironmentMap);
for (final originalVariable in originalEnvironmentModel.values) {
if (mutableUpdatedEnv.containsKey(originalVariable.key)) {
final dynamic newValue = mutableUpdatedEnv[originalVariable.key];
newValues.add(
originalVariable.copyWith(
value: newValue == null ? '' : newValue.toString(),
enabled: true,
),
);
mutableUpdatedEnv.remove(originalVariable.key);
} else {
// Variable was removed by the script (unset/clear), don't add it to newValues.
// Alternatively, you could keep it but set enabled = false:
// newValues.add(originalVariable.copyWith(enabled: false));
}
}
for (final entry in mutableUpdatedEnv.entries) {
final dynamic newValue = entry.value;
newValues.add(
EnvironmentVariableModel(
key: entry.key,
value: newValue == null ? '' : newValue.toString(),
enabled: true,
type: EnvironmentVariableType.variable,
),
);
}
ref.read(environmentsStateNotifierProvider.notifier).updateEnvironment(
originalEnvironmentModel.id,
name: originalEnvironmentModel.name,
values: newValues);
} else {
debugPrint(
"Skipped environment update as originalEnvironmentModel was null.");
if (scriptResult.updatedEnvironment.isNotEmpty) {
debugPrint(
"Warning: Pre-request script updated environment variables, but no active environment was selected to save them to.");
}
}
}
Future<void> sendRequest() async { Future<void> sendRequest() async {
final requestId = ref.read(selectedIdStateProvider); final requestId = ref.read(selectedIdStateProvider);
ref.read(codePaneVisibleStateProvider.notifier).state = false; ref.read(codePaneVisibleStateProvider.notifier).state = false;
final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme; final defaultUriScheme = ref.read(settingsProvider).defaultUriScheme;
final EnvironmentModel? originalEnvironmentModel =
ref.read(selectedEnvironmentModelProvider);
if (requestId == null || state == null) { if (requestId == null || state == null) {
return; return;
@@ -284,15 +346,7 @@ class CollectionStateNotifier
} }
if (requestModel != null && requestModel.preRequestScript.isNotEmpty) { if (requestModel != null && requestModel.preRequestScript.isNotEmpty) {
final res = await executePreRequestScript( await handlePreRequestScript(requestModel, originalEnvironmentModel);
currentRequestModel: requestModel,
activeEnvironment: {},
);
requestModel =
requestModel.copyWith(httpRequestModel: res.updatedRequest);
log(res.updatedRequest.url);
log(res.updatedRequest.headersMap.toString());
log(res.updatedRequest.body.toString());
} }
APIType apiType = requestModel!.apiType; APIType apiType = requestModel!.apiType;