Files
nimbus/resources/js/stores/request/useRequestBuilderStore.ts
Mazen Touati aa99eacd2c feat(relay): add transaction mode to requests (#49)
* feat(ui): initialize scroll masks onmount

* feat(relay): add transaction mode to requests

* test: set sqlite as default connection

* test(relay): add missing test

* chore: clearer access to implementation

* style: apply rector

* style: apply php style fixes

* test: ts type fixes
2026-01-26 01:03:55 +01:00

509 lines
17 KiB
TypeScript

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<PendingRequest | null> = ref<PendingRequest | null>(
null,
);
const activeApplication: Ref<string | null> = ref<string | null>(null);
const lastSyncedGlobalHeaders: Ref<ParameterContract[]> = 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<ParameterContract>) => {
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();
},
},
},
);