97 lines
3.9 KiB
TypeScript
97 lines
3.9 KiB
TypeScript
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod/v4";
|
|
|
|
import { permissionRequiredProcedure } from "../../trpc";
|
|
import {
|
|
getPreviewRequestSource,
|
|
previewSessionRequestSchema,
|
|
recordPreviewJournal,
|
|
resolvePreviewRequestParams,
|
|
} from "./preview-procedure-helpers";
|
|
import { executeCustomWidgetRequest } from "./request-executor";
|
|
import { hashRuntimeParams, renderRequestBody, renderRequestTarget } from "./request-manifest";
|
|
import { acquireCustomWidgetRequestLimit } from "./request-limits";
|
|
import { getPreviewJournal, getPreviewSession, setPreviewSessionLiveActions } from "./preview-sessions";
|
|
|
|
export const previewQueryProcedures = {
|
|
previewQuery: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({
|
|
mcp: {
|
|
enabled: true,
|
|
description:
|
|
"Execute one real API query from customWidget_previewCreate.queries and return its HTTP status plus parsed data so request paths and template bindings can be verified. Call once for every returned query before customWidget_create.",
|
|
},
|
|
})
|
|
.input(previewSessionRequestSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
const session = await getPreviewSession(input.sessionId, ctx.session.user.id);
|
|
const definition = session.requests[input.requestId];
|
|
if (definition?.kind !== "query")
|
|
throw new TRPCError({ code: "NOT_FOUND", message: "Preview query was not found" });
|
|
const request = { id: input.requestId, ...definition };
|
|
const resolved = getPreviewRequestSource(session, request.source);
|
|
if (!resolved) throw new TRPCError({ code: "NOT_FOUND", message: "Preview source was not found" });
|
|
const params = resolvePreviewRequestParams(request, session.options, input.params);
|
|
const targetUrl = renderRequestTarget(resolved.source.baseUrl, request, params);
|
|
const body = renderRequestBody(request, params);
|
|
const release = await acquireCustomWidgetRequestLimit({
|
|
category: "query",
|
|
userId: ctx.session.user.id,
|
|
itemId: `preview:${session.id}`,
|
|
definitionId: session.definitionId ?? `preview:${session.id}`,
|
|
});
|
|
const startedAt = Date.now();
|
|
try {
|
|
const response = await executeCustomWidgetRequest({
|
|
baseUrl: resolved.source.baseUrl,
|
|
targetUrl,
|
|
method: request.method,
|
|
body,
|
|
staticHeaders: request.headers,
|
|
auth: request.auth === "none" ? undefined : resolved.auth,
|
|
networkScope: resolved.source.networkScope,
|
|
kind: "query",
|
|
cacheKey: `custom-jsx:preview:${session.id}:${request.id}:${hashRuntimeParams(params)}`,
|
|
cacheTtlSeconds: request.cacheSeconds,
|
|
});
|
|
await recordPreviewJournal(session, {
|
|
requestId: request.id,
|
|
kind: "query",
|
|
method: request.method,
|
|
path: request.path,
|
|
status: response.status,
|
|
durationMs: Date.now() - startedAt,
|
|
simulated: false,
|
|
});
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
data: response.data,
|
|
error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}`,
|
|
};
|
|
} finally {
|
|
await release();
|
|
}
|
|
}),
|
|
|
|
setPreviewLiveActions: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.input(z.object({ sessionId: z.string().min(1), enabled: z.boolean() }))
|
|
.mutation(async ({ ctx, input }) =>
|
|
setPreviewSessionLiveActions(input.sessionId, ctx.session.user.id, input.enabled),
|
|
),
|
|
|
|
previewJournal: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({
|
|
mcp: {
|
|
enabled: true,
|
|
description: "Read the redacted request journal for a custom widget preview session.",
|
|
},
|
|
})
|
|
.input(z.object({ sessionId: z.string().min(1) }))
|
|
.query(async ({ ctx, input }) => getPreviewJournal(input.sessionId, ctx.session.user.id)),
|
|
};
|