import type { AuthorizationContract } from '@/interfaces/auth/authorization'; import { AuthorizationType } from '@/interfaces/generated'; import type { GeneratorType, PendingRequest, Request, SourceGlobalHeaders, } from '@/interfaces/http'; import { RequestBodyTypeEnum } from '@/interfaces/http'; import type { RouteDefinition } from '@/interfaces/routes/routes'; import type { ParameterContract } from '@/interfaces/ui'; import { ParameterType } from '@/interfaces/ui'; import { useConfigStore, useSettingsStore, useValueGeneratorStore } from '@/stores'; import { buildRequestUrl, getDefaultPayloadTypeForRoute } from '@/utils/request'; import { generateValueFromType } from '@/utils/value-generator/generateValueFromType'; import { defineStore } from 'pinia'; import type { Ref } from 'vue'; import { computed, ref } from 'vue'; /** * Store for managing request building and configuration. * * Handles all aspects of request construction including method, * endpoint, headers, body, query parameters, and authorization. */ export const useRequestBuilderStore = defineStore( '_requestBuilder', () => { /* * Stores. */ const settingsStore = useSettingsStore(); const configStore = useConfigStore(); const valueGeneratorStore = useValueGeneratorStore(); /* * State. */ const pendingRequestData: Ref = ref( null, ); const activeApplication: Ref = ref(null); const lastSyncedGlobalHeaders: Ref = ref< ParameterContract[] >([]); /* * Computed. */ const hasActiveRequest = computed(() => pendingRequestData.value !== null); /* * Request Building Actions. */ /** * Switches to the route definition for the specified method on the current endpoint. * * Updates schema and payload type based on the route definition for the given method. * If no route definition exists for this method on this endpoint, defaults to empty payload. */ const switchToRouteDefinitionOf = ( method: string, requestData: PendingRequest, ) => { // Find route definition for this method within the same endpoint const targetRoute = requestData.supportedRoutes.find( (route: RouteDefinition) => route.method.toUpperCase() === method, ); if (!targetRoute) { // For methods not defined for this endpoint, default to empty payload and schema requestData.payloadType = RequestBodyTypeEnum.EMPTY; requestData.schema = { shape: {}, extractionErrors: null, }; return; } // Use schema from the route definition for this method requestData.payloadType = getDefaultPayloadTypeForRoute(targetRoute); requestData.schema = targetRoute.schema; }; const getAuthorizationForNewRequest = function (): AuthorizationContract { if (pendingRequestData.value !== null) { // Re-use the same authorization from last request if exists. return pendingRequestData.value.authorization; } // Otherwise, use default authorization from settings. if ( settingsStore.preferences.defaultAuthorizationType === AuthorizationType.CurrentUser ) { return { type: AuthorizationType.CurrentUser, }; } if ( settingsStore.preferences.defaultAuthorizationType === AuthorizationType.Impersonate ) { return { type: AuthorizationType.Impersonate, value: 1, }; } if ( settingsStore.preferences.defaultAuthorizationType === AuthorizationType.Bearer ) { return { type: AuthorizationType.Bearer, value: '', }; } if ( settingsStore.preferences.defaultAuthorizationType === AuthorizationType.Basic ) { return { type: AuthorizationType.Basic, value: { username: '', password: '' }, }; } return { type: AuthorizationType.None, }; }; const getDefaultPayload = function (route: RouteDefinition): RequestBodyTypeEnum { if (settingsStore.preferences.defaultRequestBodyType === -1) { return getDefaultPayloadTypeForRoute(route); } return settingsStore.preferences.defaultRequestBodyType; }; /** * Initializes new pending request with a default state. */ const initializeRequest = ( route: RouteDefinition, availableRoutesForEndpoint: RouteDefinition[], ) => { const currentHeaders = pendingRequestData.value?.headers ?? []; const currentQueryParameters = pendingRequestData.value?.queryParameters ?? []; pendingRequestData.value = { method: route.method, endpoint: route.endpoint, headers: currentHeaders, body: {}, payloadType: getDefaultPayload(route), schema: route.schema, queryParameters: currentQueryParameters, authorization: getAuthorizationForNewRequest(), supportedRoutes: availableRoutesForEndpoint, routeDefinition: route, isProcessing: false, wasExecuted: false, durationInMs: 0, transactionMode: false, }; // Ensure global headers are synced for the fresh request useCurrentApplicationGlobalHeaders(); }; /** * Updates the HTTP method for the current request. * * Handles method changes by updating schema and payload type based on * available route definitions, falling back to empty payload for unsupported methods. */ const updateRequestMethod = (method: string) => { if (!pendingRequestData.value) { return; } const normalizedMethod = method.toUpperCase(); if (normalizedMethod === pendingRequestData.value.method) { return; // <- same method, no need to update. } pendingRequestData.value.method = normalizedMethod; // Switch to the route definition for this method if applicable. switchToRouteDefinitionOf(normalizedMethod, pendingRequestData.value); }; /** * Updates the endpoint URL for the current request. */ const updateRequestEndpoint = (endpoint: string) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.endpoint = endpoint; }; /** * Updates the headers array for the current request. */ const updateRequestHeaders = (headers: Array) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.headers = headers; }; /** * Updates the request body for the current request. */ const updateRequestBody = (body: PendingRequest['body']) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.body = body; }; /** * Updates the query parameters for the current request. */ const updateQueryParameters = (parameters: ParameterContract[]) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.queryParameters = parameters; }; /** * Updates the authorization configuration for the current request. */ const updateAuthorization = (authorization: AuthorizationContract) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.authorization = authorization; }; /** * Updates the transaction mode for the current request. */ const updateTransactionMode = (transactionMode: boolean) => { if (!pendingRequestData.value) { return; } pendingRequestData.value.transactionMode = transactionMode; }; /** * Resets the current request to null state. */ const resetRequest = () => { pendingRequestData.value = null; }; /** * Re-synchronizes global headers when switching between applications. * * Removes headers that were previously injected as global and * injects the new global headers from the current application. */ const useCurrentApplicationGlobalHeaders = () => { if (!pendingRequestData.value) { return; } const previousGlobalHeaderKeys = lastSyncedGlobalHeaders.value.map( header => header.key, ); const filteredHeaders = pendingRequestData.value.headers.filter( header => !previousGlobalHeaderKeys.includes(header.key), ); const newGlobalHeaders = configStore.headers.map( (globalHeader: SourceGlobalHeaders): ParameterContract => ({ type: ParameterType.Text, key: globalHeader.header, value: globalHeader.type === 'generator' ? generateValueFromType( globalHeader.value as GeneratorType, valueGeneratorStore, ) : String(globalHeader.value), enabled: true, }), ); pendingRequestData.value.headers = [...newGlobalHeaders, ...filteredHeaders]; lastSyncedGlobalHeaders.value = newGlobalHeaders; }; /* * Helper functions. */ /** * Builds complete request URL with query parameters. * * Constructs the full URL by combining base URL, endpoint, and * enabled query parameters for the current request. */ const getRequestUrl = (request: PendingRequest): string => { return buildRequestUrl( configStore.apiUrl, request.endpoint, request.queryParameters.filter( (parameter: ParameterContract) => parameter.enabled && parameter.key.trim() !== '', ), ); }; /** * Restores the request builder state from a historical request. */ const restoreFromHistory = (historicalRequest: Request) => { if (!pendingRequestData.value) { return; } const method = historicalRequest.method.toUpperCase(); const payloadType = historicalRequest.payloadType; // Try to find and sync the route definition const matchingRoute = pendingRequestData.value.supportedRoutes.find( route => route.method.toUpperCase() === method && route.endpoint === historicalRequest.endpoint, ); pendingRequestData.value = { ...pendingRequestData.value, method, endpoint: historicalRequest.endpoint, headers: historicalRequest.headers.map(h => ({ ...h })), queryParameters: historicalRequest.queryParameters.map(p => ({ ...p })), payloadType, // Restore body into the correct slot with reactivity in mind body: { ...pendingRequestData.value.body, [method]: { ...(pendingRequestData.value.body[method] ?? {}), [payloadType]: historicalRequest.body, }, }, // Restore authorization authorization: { ...historicalRequest.authorization, }, // Sync route definition and schema if matching route found ...(matchingRoute ? { routeDefinition: matchingRoute, schema: matchingRoute.schema, } : {}), wasExecuted: true, transactionMode: pendingRequestData.value.transactionMode ?? false, }; }; /** * Restores request state from a shareable link payload. * * This bypasses normal route initialization to directly restore * all request data from the shared payload. */ const restoreFromSharedPayload = (payload: { method: string; endpoint: string; headers: Array<{ key: string; value: string | number | boolean | null; }>; queryParameters: Array<{ key: string; value: string; type?: 'text' | 'file'; }>; body: PendingRequest['body']; payloadType: string; authorization: { type: string; value?: string | number | { username: string; password: string }; }; durationInMs?: number; wasExecuted?: boolean; }) => { const wasExecuted = payload.wasExecuted ?? payload.durationInMs !== undefined; pendingRequestData.value = { method: payload.method.toUpperCase(), endpoint: payload.endpoint, headers: payload.headers.map(header => ({ key: header.key, value: String(header.value ?? ''), type: ParameterType.Text, enabled: true, })), body: payload.body, payloadType: payload.payloadType as RequestBodyTypeEnum, schema: { shape: {}, extractionErrors: null, }, queryParameters: payload.queryParameters.map(param => ({ key: param.key, value: param.value, type: param.type === 'file' ? ParameterType.File : ParameterType.Text, enabled: true, })), authorization: { type: payload.authorization.type as AuthorizationType, value: payload.authorization.value, } as AuthorizationContract, supportedRoutes: [], routeDefinition: { endpoint: payload.endpoint, method: payload.method.toUpperCase(), schema: { shape: {}, extractionErrors: null, }, shortEndpoint: payload.endpoint, }, isProcessing: false, wasExecuted, durationInMs: payload.durationInMs ?? 0, transactionMode: false, }; }; /* * Startup Logic. */ const syncGlobalHeadersWhenApplicable = () => { if (activeApplication.value === configStore.activeApplication) { return; } useCurrentApplicationGlobalHeaders(); // Update the active application for the next time. // It will be preserved because we persist the store state. activeApplication.value = configStore.activeApplication; }; return { // State pendingRequestData, activeApplication, lastSyncedGlobalHeaders, // Computed hasActiveRequest, // Actions initializeRequest, updateRequestMethod, updateRequestEndpoint, updateRequestHeaders, updateRequestBody, updateQueryParameters, updateAuthorization, updateTransactionMode, resetRequest, syncGlobalHeadersWhenApplicable, getRequestUrl, restoreFromHistory, restoreFromSharedPayload, }; }, { persist: { afterHydrate: context => { context.store.syncGlobalHeadersWhenApplicable(); }, }, }, );