Files
homarr/packages/api/src/router/custom-widget/preview-action-procedures.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

114 lines
4.8 KiB
TypeScript

import { TRPCError } from "@trpc/server";
import { z } from "zod/v4";
import { permissionRequiredProcedure } from "../../trpc";
import {
getPreviewRequestSource,
getPreviewRequestSourceConfigurationFailure,
previewSessionRequestSchema,
recordPreviewJournal,
} from "./preview-procedure-helpers";
import { executeCustomWidgetRequest, invalidateCustomWidgetResponseCache } from "./request-executor";
import { renderRequestBody, renderRequestTarget, resolveCustomWidgetRequestValues } from "./request-manifest";
import { acquireCustomWidgetRequestLimit } from "./request-limits";
import { getPreviewSession } from "./preview-sessions";
export const previewActionProcedures = {
previewAction: permissionRequiredProcedure
.requiresPermission("admin")
.meta({
mcp: {
enabled: true,
description:
"Test one action from the current preview revision. Supply its required params and keep preview actions simulated unless the user explicitly enabled live actions. Verify confirmation, permission, and invalidation metadata before createFromPreview or updateFromPreview persistence.",
},
})
.input(previewSessionRequestSchema.extend({ confirmed: z.boolean().optional() }))
.mutation(async ({ ctx, input }) => {
const session = await getPreviewSession(input.sessionId, ctx.session.user.id);
const definition = session.requests[input.requestId];
if (definition?.kind !== "action")
throw new TRPCError({ code: "NOT_FOUND", message: "Preview action was not found" });
const request = { id: input.requestId, ...definition };
const sourceConfigurationFailure = getPreviewRequestSourceConfigurationFailure(session, request);
if (sourceConfigurationFailure) return { ...sourceConfigurationFailure, simulated: false as const };
const params = resolveCustomWidgetRequestValues(request, session.options, input.params);
const body = renderRequestBody(request, params);
if (!session.liveActions) {
const resolved = await getPreviewRequestSource(ctx, session, request);
if (!resolved) throw new TRPCError({ code: "NOT_FOUND", message: "Preview source was not found" });
renderRequestTarget(resolved.baseUrl, request, params);
await recordPreviewJournal(session, {
requestId: request.id,
kind: "action",
method: request.method,
path: request.path,
status: null,
durationMs: 0,
simulated: true,
});
return {
sessionId: session.id,
requestId: request.id,
sourceId: request.source,
ok: true,
status: 0,
statusText: "Simulated",
data: null,
simulated: true as const,
};
}
if ((request.confirmation || request.method === "DELETE") && input.confirmed !== true) {
throw new TRPCError({ code: "PRECONDITION_FAILED", message: "This action requires confirmation" });
}
const release = await acquireCustomWidgetRequestLimit({
category: request.method === "DELETE" ? "delete" : "action",
userId: ctx.session.user.id,
itemId: `preview:${session.id}`,
definitionId: session.definitionId ?? `preview:${session.id}`,
});
const startedAt = Date.now();
try {
const resolved = await getPreviewRequestSource(ctx, session, request);
if (!resolved) throw new TRPCError({ code: "NOT_FOUND", message: "Preview source was not found" });
const targetUrl = renderRequestTarget(resolved.baseUrl, request, params);
const response = await executeCustomWidgetRequest({
...resolved,
targetUrl,
method: request.method,
body,
staticHeaders: request.headers,
auth: request.auth === "none" ? undefined : resolved.auth,
kind: "action",
});
if (response.ok && request.invalidates?.length) {
invalidateCustomWidgetResponseCache(
request.invalidates.map((requestId) => `custom-jsx:preview:${session.id}:${requestId}:`),
);
}
await recordPreviewJournal(session, {
requestId: request.id,
kind: "action",
method: request.method,
path: request.path,
status: response.status,
durationMs: Date.now() - startedAt,
simulated: false,
});
return {
sessionId: session.id,
requestId: request.id,
sourceId: request.source,
ok: response.ok,
status: response.status,
statusText: response.statusText,
data: response.data,
error: response.ok ? undefined : `HTTP ${response.status}: ${response.statusText}`,
simulated: false as const,
};
} finally {
await release();
}
}),
};