feat: expose request header configuration
This commit is contained in:
@@ -2,17 +2,17 @@
|
||||
|
||||
## Current
|
||||
|
||||
### `feat/ui-ux-pass`
|
||||
### `feat/header-fields`
|
||||
|
||||
Status: completed
|
||||
|
||||
DoD:
|
||||
|
||||
- shell выглядит как цельный продуктовый интерфейс, а не как набор отдельных секций
|
||||
- operation list показывает обзор состояния системы, а не только список карточек
|
||||
- create page объясняет оператору различия между `REST`, `GraphQL` и `gRPC`
|
||||
- ключевые сценарии имеют пустые состояния и более понятную визуальную иерархию
|
||||
- UI build и tests остаются зелеными после UX-pass
|
||||
- форма создания явно поддерживает настройку headers, а не прячет их только в сыром JSON
|
||||
- `execution headers` доступны для всех протоколов
|
||||
- `static headers` доступны для REST target
|
||||
- detail view показывает сохраненные headers
|
||||
- UI build и tests остаются зелеными
|
||||
|
||||
## Next
|
||||
|
||||
|
||||
@@ -4,11 +4,33 @@ import { buildOperationPayload, defaultOperationFormValues } from "./model";
|
||||
|
||||
describe("buildOperationPayload", () => {
|
||||
it("creates a REST operation payload from form values", () => {
|
||||
const payload = buildOperationPayload(defaultOperationFormValues);
|
||||
const payload = buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
restStaticHeadersText: JSON.stringify(
|
||||
{
|
||||
"x-app-source": "rmcp",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
executionHeadersText: JSON.stringify(
|
||||
{
|
||||
"x-trace-id": "trace-123",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
});
|
||||
|
||||
expect(payload.protocol).toBe("rest");
|
||||
expect(payload.target.kind).toBe("rest");
|
||||
expect(payload.target.method).toBe("POST");
|
||||
expect(payload.target.static_headers).toEqual({
|
||||
"x-app-source": "rmcp",
|
||||
});
|
||||
expect(payload.execution_config.headers).toEqual({
|
||||
"x-trace-id": "trace-123",
|
||||
});
|
||||
expect(payload.input_mapping).toEqual({
|
||||
rules: [
|
||||
{
|
||||
@@ -111,4 +133,13 @@ describe("buildOperationPayload", () => {
|
||||
}),
|
||||
).toThrow(/Input schema contains invalid JSON/);
|
||||
});
|
||||
|
||||
it("throws when header values are not string maps", () => {
|
||||
expect(() =>
|
||||
buildOperationPayload({
|
||||
...defaultOperationFormValues,
|
||||
executionHeadersText: JSON.stringify({ "x-trace-id": 42 }, null, 2),
|
||||
}),
|
||||
).toThrow(/Execution headers.x-trace-id must be a string value/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ export const operationFormSchema = z
|
||||
restBaseUrl: z.string(),
|
||||
restMethod: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]),
|
||||
restPathTemplate: z.string(),
|
||||
restStaticHeadersText: jsonTextSchema,
|
||||
graphqlEndpoint: z.string(),
|
||||
graphqlOperationType: z.enum(["query", "mutation"]),
|
||||
graphqlOperationName: z.string(),
|
||||
@@ -32,6 +33,7 @@ export const operationFormSchema = z
|
||||
outputSchemaText: jsonTextSchema,
|
||||
inputMappingText: jsonTextSchema,
|
||||
outputMappingText: jsonTextSchema,
|
||||
executionHeadersText: jsonTextSchema,
|
||||
executionConfigText: jsonTextSchema,
|
||||
})
|
||||
.superRefine((values, context) => {
|
||||
@@ -111,6 +113,22 @@ export const operationFormSchema = z
|
||||
|
||||
export type OperationFormValues = z.infer<typeof operationFormSchema>;
|
||||
|
||||
function parseStringMap(raw: string, fieldName: string) {
|
||||
const value = safeParseJson<Record<string, unknown>>(raw, fieldName);
|
||||
|
||||
if (value === null || Array.isArray(value) || typeof value !== "object") {
|
||||
throw new Error(`${fieldName} must be a JSON object`);
|
||||
}
|
||||
|
||||
for (const [key, entry] of Object.entries(value)) {
|
||||
if (typeof entry !== "string") {
|
||||
throw new Error(`${fieldName}.${key} must be a string value`);
|
||||
}
|
||||
}
|
||||
|
||||
return value as Record<string, string>;
|
||||
}
|
||||
|
||||
function defaultInputSchemaText() {
|
||||
return JSON.stringify(
|
||||
{
|
||||
@@ -265,6 +283,7 @@ export const defaultOperationFormValues: OperationFormValues = {
|
||||
restBaseUrl: "https://api.example.com",
|
||||
restMethod: "POST",
|
||||
restPathTemplate: "/v1/leads",
|
||||
restStaticHeadersText: JSON.stringify({}, null, 2),
|
||||
graphqlEndpoint: "https://api.example.com/graphql",
|
||||
graphqlOperationType: "mutation",
|
||||
graphqlOperationName: "CreateLead",
|
||||
@@ -285,6 +304,7 @@ export const defaultOperationFormValues: OperationFormValues = {
|
||||
outputSchemaText: defaultOutputSchemaText(),
|
||||
inputMappingText: defaultRestInputMappingText(),
|
||||
outputMappingText: defaultRestOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
executionConfigText: defaultExecutionConfigText(),
|
||||
};
|
||||
|
||||
@@ -300,6 +320,8 @@ export function getProtocolPreset(
|
||||
toolDescription: "Creates a CRM lead from MCP input fields.",
|
||||
inputMappingText: defaultRestInputMappingText(),
|
||||
outputMappingText: defaultRestOutputMappingText(),
|
||||
restStaticHeadersText: JSON.stringify({}, null, 2),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
case "graphql":
|
||||
return {
|
||||
@@ -310,6 +332,7 @@ export function getProtocolPreset(
|
||||
"Executes a fixed GraphQL mutation and returns the selected lead payload.",
|
||||
inputMappingText: defaultGraphqlInputMappingText(),
|
||||
outputMappingText: defaultGraphqlOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
case "grpc":
|
||||
return {
|
||||
@@ -320,6 +343,7 @@ export function getProtocolPreset(
|
||||
"Executes a unary gRPC method using a descriptor-driven request contract.",
|
||||
inputMappingText: defaultGrpcInputMappingText(),
|
||||
outputMappingText: defaultGrpcOutputMappingText(),
|
||||
executionHeadersText: JSON.stringify({}, null, 2),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -330,10 +354,18 @@ export function buildOperationPayload(rawValues: OperationFormValues) {
|
||||
const outputSchema = safeParseJson(values.outputSchemaText, "Output schema");
|
||||
const inputMapping = safeParseJson(values.inputMappingText, "Input mapping");
|
||||
const outputMapping = safeParseJson(values.outputMappingText, "Output mapping");
|
||||
const executionConfig = safeParseJson(
|
||||
const executionHeaders = parseStringMap(
|
||||
values.executionHeadersText,
|
||||
"Execution headers",
|
||||
);
|
||||
const executionConfig = safeParseJson<Record<string, unknown>>(
|
||||
values.executionConfigText,
|
||||
"Execution config",
|
||||
);
|
||||
const normalizedExecutionConfig = {
|
||||
...executionConfig,
|
||||
headers: executionHeaders,
|
||||
};
|
||||
|
||||
const basePayload = {
|
||||
name: values.name,
|
||||
@@ -342,7 +374,7 @@ export function buildOperationPayload(rawValues: OperationFormValues) {
|
||||
output_schema: outputSchema,
|
||||
input_mapping: inputMapping,
|
||||
output_mapping: outputMapping,
|
||||
execution_config: executionConfig,
|
||||
execution_config: normalizedExecutionConfig,
|
||||
tool_description: {
|
||||
title: values.toolTitle,
|
||||
description: values.toolDescription,
|
||||
@@ -361,7 +393,10 @@ export function buildOperationPayload(rawValues: OperationFormValues) {
|
||||
base_url: values.restBaseUrl,
|
||||
method: values.restMethod,
|
||||
path_template: values.restPathTemplate,
|
||||
static_headers: {},
|
||||
static_headers: parseStringMap(
|
||||
values.restStaticHeadersText,
|
||||
"REST static headers",
|
||||
),
|
||||
},
|
||||
};
|
||||
case "graphql":
|
||||
|
||||
@@ -41,6 +41,13 @@ const restFields: FieldConfig[] = [
|
||||
{ name: "restBaseUrl", label: "Base URL" },
|
||||
{ name: "restMethod", label: "HTTP method" },
|
||||
{ name: "restPathTemplate", label: "Path template" },
|
||||
{
|
||||
name: "restStaticHeadersText",
|
||||
label: "Static headers",
|
||||
description: "Always sent for this REST target before dynamic request headers are merged.",
|
||||
rows: 8,
|
||||
wide: true,
|
||||
},
|
||||
];
|
||||
|
||||
const graphqlFields: FieldConfig[] = [
|
||||
@@ -89,6 +96,13 @@ const schemaFields: FieldConfig[] = [
|
||||
const mappingFields: FieldConfig[] = [
|
||||
{ name: "inputMappingText", label: "Input mapping", rows: 12, wide: true },
|
||||
{ name: "outputMappingText", label: "Output mapping", rows: 12, wide: true },
|
||||
{
|
||||
name: "executionHeadersText",
|
||||
label: "Execution headers",
|
||||
description: "Shared transport headers available for REST, GraphQL and gRPC requests.",
|
||||
rows: 8,
|
||||
wide: true,
|
||||
},
|
||||
{
|
||||
name: "executionConfigText",
|
||||
label: "Execution config",
|
||||
|
||||
@@ -40,6 +40,25 @@ function describeTarget(target: OperationTarget) {
|
||||
}
|
||||
}
|
||||
|
||||
function readObjectRecord(value: unknown) {
|
||||
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function readExecutionHeaders(value: unknown) {
|
||||
const config = readObjectRecord(value);
|
||||
const headers = config.headers;
|
||||
|
||||
if (headers === null || typeof headers !== "object" || Array.isArray(headers)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return headers as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function draftSubtitle(target: OperationTarget) {
|
||||
switch (target.kind) {
|
||||
case "rest":
|
||||
@@ -147,6 +166,16 @@ export function OperationDetailPage() {
|
||||
<h3>Execution config</h3>
|
||||
<pre>{JSON.stringify(snapshot.execution_config, null, 2)}</pre>
|
||||
</div>
|
||||
<div className="result-panel">
|
||||
<h3>Transport headers</h3>
|
||||
<pre>{JSON.stringify(readExecutionHeaders(snapshot.execution_config), null, 2)}</pre>
|
||||
</div>
|
||||
{snapshot.target.kind === "rest" ? (
|
||||
<div className="result-panel">
|
||||
<h3>REST static headers</h3>
|
||||
<pre>{JSON.stringify(snapshot.target.static_headers ?? {}, null, 2)}</pre>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</PageSection>
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user