+12








fcd7c979df
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>
130 lines
5.1 KiB
TypeScript
130 lines
5.1 KiB
TypeScript
import { assertCustomWidgetIntegrationBindings } from "./source-resolver";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { z } from "zod/v4";
|
|
|
|
import { createId } from "@homarr/common";
|
|
import { createLogger } from "@homarr/core/infrastructure/logs";
|
|
import { eq } from "@homarr/db";
|
|
import { customWidgetDefinitions } from "@homarr/db/schema";
|
|
import {
|
|
customWidgetDefinitionSchema,
|
|
customWidgetUpdateSchema,
|
|
normalizeCustomWidgetAuthoringUpdate,
|
|
} from "@homarr/custom-widgets/core";
|
|
|
|
import { createTRPCRouter, permissionRequiredProcedure } from "../../trpc";
|
|
import { parseCustomWidgetAuthoringInput } from "./authoring-validation";
|
|
import { creationProcedures } from "./creation-procedures";
|
|
import { updateCustomWidgetDefinition } from "./definition-update";
|
|
import { managementQueryProcedures } from "./management-queries";
|
|
import { metadataProcedures } from "./metadata-procedures";
|
|
import { previewActionProcedures } from "./preview-action-procedures";
|
|
import { previewBaseProcedures } from "./preview-base-procedures";
|
|
import { previewQueryProcedures } from "./preview-query-procedures";
|
|
import {
|
|
parseStoredCustomWidgetDefinition,
|
|
safeParseStoredCustomWidgetDefinition,
|
|
serializeCustomWidgetDefinition,
|
|
} from "./stored-definition";
|
|
import { templateProcedures } from "./template-procedures";
|
|
import { transferProcedures } from "./transfer-procedures";
|
|
import { assertSecretSources } from "./secret-policy";
|
|
import { secretProcedures } from "./secret-procedures";
|
|
import { workshopProcedures } from "./workshop-procedures";
|
|
|
|
const logger = createLogger({ module: "custom-widget" });
|
|
|
|
export const customWidgetRouter = createTRPCRouter({
|
|
...metadataProcedures,
|
|
...managementQueryProcedures,
|
|
...creationProcedures,
|
|
|
|
update: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({
|
|
mcp: {
|
|
enabled: true,
|
|
description:
|
|
"Update one Custom JSX widget. Prefer templateLines for multiline JSX changes. Returns a client-navigable edit link.",
|
|
},
|
|
})
|
|
.input(customWidgetUpdateSchema)
|
|
.mutation(async ({ ctx, input }) => {
|
|
const existing = await ctx.db.query.customWidgetDefinitions.findFirst({
|
|
where: eq(customWidgetDefinitions.id, input.id),
|
|
});
|
|
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
|
|
|
const current = parseStoredCustomWidgetDefinition(existing);
|
|
const { id, secrets, ...authoringChanges } = input;
|
|
const definition = parseCustomWidgetAuthoringInput(() => {
|
|
const changes = normalizeCustomWidgetAuthoringUpdate(authoringChanges);
|
|
return customWidgetDefinitionSchema.parse({ ...current, ...changes });
|
|
});
|
|
await assertCustomWidgetIntegrationBindings(ctx, definition.sources);
|
|
if (secrets) assertSecretSources(definition.sources, secrets);
|
|
await updateCustomWidgetDefinition(ctx.db, { id, definition, secrets });
|
|
logger.info("Updated custom widget definition", { id });
|
|
return { id, managementPath: `/manage/custom-widgets/edit/${id}` };
|
|
}),
|
|
|
|
...secretProcedures,
|
|
...workshopProcedures,
|
|
|
|
toggleEnabled: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.input(z.object({ id: z.string(), enabled: z.boolean() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
if (input.enabled) {
|
|
const definition = await ctx.db.query.customWidgetDefinitions.findFirst({
|
|
where: eq(customWidgetDefinitions.id, input.id),
|
|
});
|
|
if (!definition) throw new TRPCError({ code: "NOT_FOUND" });
|
|
if (!safeParseStoredCustomWidgetDefinition(definition).success) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Invalid custom widget definition cannot be enabled",
|
|
});
|
|
}
|
|
}
|
|
|
|
await ctx.db
|
|
.update(customWidgetDefinitions)
|
|
.set({ enabled: input.enabled, updatedAt: new Date() })
|
|
.where(eq(customWidgetDefinitions.id, input.id));
|
|
}),
|
|
|
|
delete: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.meta({ mcp: { enabled: true, description: "Delete one Custom JSX widget." } })
|
|
.input(z.object({ id: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
await ctx.db.delete(customWidgetDefinitions).where(eq(customWidgetDefinitions.id, input.id));
|
|
logger.info("Deleted custom widget definition", { id: input.id });
|
|
}),
|
|
|
|
duplicate: permissionRequiredProcedure
|
|
.requiresPermission("admin")
|
|
.input(z.object({ id: z.string() }))
|
|
.mutation(async ({ ctx, input }) => {
|
|
const existing = await ctx.db.query.customWidgetDefinitions.findFirst({
|
|
where: eq(customWidgetDefinitions.id, input.id),
|
|
});
|
|
if (!existing) throw new TRPCError({ code: "NOT_FOUND" });
|
|
const current = parseStoredCustomWidgetDefinition(existing);
|
|
const id = createId();
|
|
await ctx.db.insert(customWidgetDefinitions).values({
|
|
id,
|
|
...serializeCustomWidgetDefinition({ ...current, name: `${current.name} (copy)` }),
|
|
creatorId: ctx.session.user.id,
|
|
});
|
|
return { id, name: `${current.name} (copy)` };
|
|
}),
|
|
|
|
...templateProcedures,
|
|
...transferProcedures,
|
|
...previewBaseProcedures,
|
|
...previewQueryProcedures,
|
|
...previewActionProcedures,
|
|
});
|