Files
nimbus/resources/js/composables/request/useRequestBody.ts
Mazen Touati e1b844cee0 feat(history): add history viewer and rewind (#38)
* feat(ui): add `input group` base component

* feat(history): add history viewer and rewind

* test: update selector snapshot

* test: add PW base page

* style: apply TS style fixes

* chore(history): request history wiki

* chore(history): remove unwanted symbol

* chore: fix type

* style: apply TS style fixes
2026-01-17 20:50:00 +01:00

214 lines
5.7 KiB
TypeScript

import { PendingRequest, RequestBodyTypeEnum, RequestHeader } from '@/interfaces/http';
import { useRequestStore } from '@/stores';
import {
generatePlaceholderPayload,
generateRandomPayload,
serializeSchemaPayload,
} from '@/utils/payload';
import { types, TypeShape } from '@/utils/request/content-type-header-generator';
import { computed, onMounted, ref, watch } from 'vue';
export function useRequestBody() {
/*
* Stores & dependencies.
*/
const requestStore = useRequestStore();
const generateRandomPayloadFn = generateRandomPayload;
const generatePlaceholderPayloadFn = generatePlaceholderPayload;
/*
* State.
*/
const payloadType = ref<RequestBodyTypeEnum>(RequestBodyTypeEnum.EMPTY);
const payload = ref<FormData | string | null>(null);
/*
* Computed.
*/
const pendingRequestData = computed(() => requestStore.pendingRequestData);
const supportsAutoFill = computed(() => {
return (
types.find(type => type.id === payloadType.value)?.autoFillable === true &&
pendingRequestData.value?.schema?.shape !== undefined
);
});
/*
* Actions.
*/
/**
* Generates the current payload based on request data and type.
*
* Returns the memoized payload for the current method and type, or generates
* a placeholder payload from the schema if none exists.
*/
const generateCurrentPayload = (): FormData | string | null => {
if (!pendingRequestData.value) {
return null;
}
if (payloadType.value === RequestBodyTypeEnum.EMPTY) {
return null;
}
const method = pendingRequestData.value.method;
const body = pendingRequestData.value.body;
// Use optional chaining and nullish coalescing for cleaner access
const memoizedBody =
(body as PendingRequest['body'])?.[method]?.[payloadType.value] ?? null;
if (memoizedBody) {
return memoizedBody;
}
// If we don't have the value memoized, we make up a new placeholder initial state.
const JSONSchema7 = pendingRequestData.value?.schema?.shape;
if (!JSONSchema7) {
return null;
}
const placeholderPayload = generatePlaceholderPayloadFn(JSONSchema7);
// Store and return the serialized payload
return serializeSchemaPayload(placeholderPayload, payloadType.value);
};
/**
* Initializes payload type from existing Content-Type headers.
*
* Reads the current Content-Type header and sets the payload type
* to match the corresponding MIME type if found.
*/
const initializePayloadTypeFromHeaders = () => {
const currentContentType: RequestHeader | undefined =
pendingRequestData.value?.headers.find(
(header: RequestHeader) => header.key === 'Content-Type',
);
if (!currentContentType) {
return;
}
const matchingTypeFromContentType: TypeShape | undefined = types.find(
type => type.mimeType === currentContentType.value,
);
if (!matchingTypeFromContentType) {
return;
}
payloadType.value = matchingTypeFromContentType.id;
};
/**
* Generates and applies random data to the request body.
*
* Uses the schema shape to generate realistic random data and applies
* it to the current payload, updating the request store.
*/
const autofill = () => {
if (!pendingRequestData.value) {
return;
}
const schemaShape = pendingRequestData.value.schema?.shape;
if (!schemaShape) {
return;
}
const generatedPayload = generateRandomPayloadFn(schemaShape);
const serializedPayload = serializeSchemaPayload(
generatedPayload,
payloadType.value,
);
payload.value = serializedPayload;
};
/*
* Watchers.
*/
watch(
pendingRequestData,
newValue => {
payloadType.value = newValue?.payloadType ?? RequestBodyTypeEnum.EMPTY;
payload.value = generateCurrentPayload();
},
{ deep: true },
);
watch(payloadType, newValue => {
if (pendingRequestData.value === null) {
payload.value = generateCurrentPayload();
return;
}
// Switch between payloads based on the current body type,
// Meaning that each tab will have its state.
payload.value = generateCurrentPayload();
pendingRequestData.value.payloadType = newValue;
});
watch(
payload,
() => {
if (!pendingRequestData.value) {
return;
}
const method = pendingRequestData.value.method;
// Initialize body as object if it's null
if (!pendingRequestData.value.body) {
pendingRequestData.value.body = {} as PendingRequest['body'];
}
const body = pendingRequestData.value.body as PendingRequest['body'];
if (!body[method]) {
body[method] = {};
}
body[method][payloadType.value] = payload.value;
},
{ deep: true },
);
/*
* Lifecycle.
*/
onMounted(() => {
initializePayloadTypeFromHeaders();
payload.value = generateCurrentPayload();
});
return {
// State
payloadType,
payload,
// Computed
pendingRequestData,
supportsAutoFill,
// Actions
autofill,
generateCurrentPayload,
initializePayloadTypeFromHeaders,
// Constants
types,
};
}