mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
fix: upgrade to Next.js 16.2.1 and resolve bundling issues
- Fixed client bundle contamination by moving child_process-dependent code - Created standalone /api/robots/command route for SSH robot commands - Created plugins router to replace robots.plugins for plugin management - Added getStudyPlugins procedure to studies router - Fixed trial.studyId references to trial.experiment.studyId - Updated WizardInterface to use REST API for robot commands
This commit is contained in:
@@ -8,6 +8,9 @@ import type {
|
||||
} from "~/lib/experiment-designer/types";
|
||||
import { api } from "~/trpc/server";
|
||||
import { DesignerPageClient } from "./DesignerPageClient";
|
||||
import { db } from "~/server/db";
|
||||
import { studyPlugins, plugins } from "~/server/db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
|
||||
interface ExperimentDesignerPageProps {
|
||||
params: Promise<{
|
||||
@@ -74,10 +77,20 @@ export default async function ExperimentDesignerPage({
|
||||
actionDefinitions: Array<{ id: string }> | null;
|
||||
};
|
||||
};
|
||||
const rawInstalledPluginsUnknown: unknown =
|
||||
await api.robots.plugins.getStudyPlugins({
|
||||
studyId: experiment.study.id,
|
||||
});
|
||||
const installedPluginsResult = await db
|
||||
.select({
|
||||
plugin: {
|
||||
id: plugins.id,
|
||||
name: plugins.name,
|
||||
version: plugins.version,
|
||||
actionDefinitions: plugins.actionDefinitions,
|
||||
},
|
||||
})
|
||||
.from(studyPlugins)
|
||||
.innerJoin(plugins, eq(studyPlugins.pluginId, plugins.id))
|
||||
.where(eq(studyPlugins.studyId, experiment.study.id))
|
||||
.orderBy(desc(studyPlugins.installedAt));
|
||||
const rawInstalledPluginsUnknown = installedPluginsResult;
|
||||
|
||||
function asRecord(v: unknown): Record<string, unknown> | null {
|
||||
return v && typeof v === "object"
|
||||
|
||||
118
src/app/api/robots/command/route.ts
Normal file
118
src/app/api/robots/command/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "~/lib/auth";
|
||||
import { db } from "~/server/db";
|
||||
import { studyMembers } from "~/server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth.api.getSession({
|
||||
headers: request.headers,
|
||||
});
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { action, studyId, robotId, parameters } = body;
|
||||
|
||||
// Verify user has access to the study
|
||||
const membership = await db.query.studyMembers.findFirst({
|
||||
where: and(
|
||||
eq(studyMembers.studyId, studyId),
|
||||
eq(studyMembers.userId, session.user.id),
|
||||
),
|
||||
});
|
||||
|
||||
if (!membership || !["owner", "researcher"].includes(membership.role)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Insufficient permissions" },
|
||||
{ status: 403 },
|
||||
);
|
||||
}
|
||||
|
||||
const robotIp =
|
||||
process.env.NAO_ROBOT_IP || process.env.NAO_IP || "134.82.159.168";
|
||||
const password = process.env.NAO_PASSWORD || "robolab";
|
||||
|
||||
switch (action) {
|
||||
case "initialize": {
|
||||
console.log(`[Robots API] Initializing robot at ${robotIp}`);
|
||||
|
||||
const disableAlCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; al = naoqi.ALProxy('ALAutonomousLife', '127.0.0.1', 9559); al.setState('disabled')\\""`;
|
||||
|
||||
const wakeUpCmd = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`;
|
||||
|
||||
await execAsync(disableAlCmd).catch((e) =>
|
||||
console.warn("AL disable failed (non-critical/already disabled):", e),
|
||||
);
|
||||
|
||||
await execAsync(wakeUpCmd);
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
case "executeSystemAction": {
|
||||
const { id, parameters: actionParams } = parameters ?? {};
|
||||
console.log(`[Robots API] Executing system action ${id}`);
|
||||
|
||||
let command = "";
|
||||
|
||||
switch (id) {
|
||||
case "say_with_emotion":
|
||||
case "say_text_with_emotion": {
|
||||
const text = String(actionParams?.text || "Hello");
|
||||
const emotion = String(actionParams?.emotion || "happy");
|
||||
const tag =
|
||||
emotion === "happy"
|
||||
? "^joyful"
|
||||
: emotion === "sad"
|
||||
? "^sad"
|
||||
: emotion === "thinking"
|
||||
? "^thoughtful"
|
||||
: "^joyful";
|
||||
|
||||
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; s = naoqi.ALProxy('ALAnimatedSpeech', '127.0.0.1', 9559); s.say('${tag} ${text.replace(/'/g, "\\'")}')\\""`;
|
||||
break;
|
||||
}
|
||||
|
||||
case "wake_up":
|
||||
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.wakeUp()\\""`;
|
||||
break;
|
||||
|
||||
case "rest":
|
||||
command = `sshpass -p "${password}" ssh -o StrictHostKeyChecking=no "nao@${robotIp}" "python2 -c \\"import sys; sys.path.append('/opt/aldebaran/lib/python2.7/site-packages'); import naoqi; m = naoqi.ALProxy('ALMotion', '127.0.0.1', 9559); m.rest()\\""`;
|
||||
break;
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `System action ${id} not implemented` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
await execAsync(command);
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
|
||||
default:
|
||||
return NextResponse.json(
|
||||
{ error: `Unknown action: ${action}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Robots API] Error:", error);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: error instanceof Error ? error.message : "Internal server error",
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,7 @@ export function DesignerRoot({
|
||||
},
|
||||
});
|
||||
|
||||
const { data: studyPluginsRaw } = api.robots.plugins.getStudyPlugins.useQuery(
|
||||
const { data: studyPluginsRaw } = api.studies.getStudyPlugins.useQuery(
|
||||
{ studyId: experiment?.studyId ?? "" },
|
||||
{ enabled: !!experiment?.studyId },
|
||||
);
|
||||
|
||||
@@ -197,22 +197,21 @@ export function PluginStoreBrowse() {
|
||||
) as { data: Array<{ id: string; url: string; name: string }> | undefined };
|
||||
|
||||
// Get installed plugins for current study
|
||||
const { data: installedPlugins } =
|
||||
api.robots.plugins.getStudyPlugins.useQuery(
|
||||
{
|
||||
studyId: selectedStudyId!,
|
||||
},
|
||||
{
|
||||
enabled: !!selectedStudyId,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
const { data: installedPlugins } = api.studies.getStudyPlugins.useQuery(
|
||||
{
|
||||
studyId: selectedStudyId!,
|
||||
},
|
||||
{
|
||||
enabled: !!selectedStudyId,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
);
|
||||
|
||||
const {
|
||||
data: availablePlugins,
|
||||
isLoading,
|
||||
error,
|
||||
} = api.robots.plugins.list.useQuery(
|
||||
} = api.plugins.list.useQuery(
|
||||
{
|
||||
status:
|
||||
statusFilter === "all"
|
||||
@@ -228,12 +227,12 @@ export function PluginStoreBrowse() {
|
||||
|
||||
const utils = api.useUtils();
|
||||
|
||||
const installPluginMutation = api.robots.plugins.install.useMutation({
|
||||
const installPluginMutation = api.plugins.install.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Plugin installed successfully!");
|
||||
// Invalidate both plugin queries to refresh the UI
|
||||
void utils.robots.plugins.list.invalidate();
|
||||
void utils.robots.plugins.getStudyPlugins.invalidate();
|
||||
void utils.plugins.list.invalidate();
|
||||
void utils.studies.getStudyPlugins.invalidate();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || "Failed to install plugin");
|
||||
@@ -430,7 +429,7 @@ export function PluginStoreBrowse() {
|
||||
"An error occurred while loading the plugin store."}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => void utils.robots.plugins.list.refetch()}
|
||||
onClick={() => void utils.plugins.list.refetch()}
|
||||
variant="outline"
|
||||
>
|
||||
Try Again
|
||||
|
||||
@@ -90,12 +90,12 @@ function PluginActionsCell({ plugin }: { plugin: Plugin }) {
|
||||
const { selectedStudyId } = useStudyContext();
|
||||
const utils = api.useUtils();
|
||||
|
||||
const uninstallMutation = api.robots.plugins.uninstall.useMutation({
|
||||
const uninstallMutation = api.plugins.uninstall.useMutation({
|
||||
onSuccess: () => {
|
||||
toast.success("Plugin uninstalled successfully");
|
||||
// Invalidate plugin queries to refresh the UI
|
||||
void utils.robots.plugins.getStudyPlugins.invalidate();
|
||||
void utils.robots.plugins.list.invalidate();
|
||||
void utils.studies.getStudyPlugins.invalidate();
|
||||
void utils.plugins.list.invalidate();
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || "Failed to uninstall plugin");
|
||||
|
||||
@@ -28,7 +28,7 @@ export function PluginsDataTable() {
|
||||
isLoading,
|
||||
error,
|
||||
refetch,
|
||||
} = api.robots.plugins.getStudyPlugins.useQuery(
|
||||
} = api.studies.getStudyPlugins.useQuery(
|
||||
{
|
||||
studyId: selectedStudyId!,
|
||||
},
|
||||
|
||||
@@ -182,7 +182,7 @@ export function RobotActionsPanel({
|
||||
|
||||
// Get installed plugins for the study
|
||||
const { data: plugins = [], isLoading } =
|
||||
api.robots.plugins.getStudyPlugins.useQuery({
|
||||
api.studies.getStudyPlugins.useQuery({
|
||||
studyId,
|
||||
});
|
||||
|
||||
|
||||
@@ -167,19 +167,36 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
},
|
||||
});
|
||||
|
||||
// Robot initialization mutation (for startup routine)
|
||||
const initializeRobotMutation = api.robots.plugins.initialize.useMutation({
|
||||
onSuccess: () => {
|
||||
const [isInitializing, setIsInitializing] = useState(false);
|
||||
|
||||
const initializeRobot = async () => {
|
||||
setIsInitializing(true);
|
||||
try {
|
||||
const response = await fetch("/api/robots/command", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "initialize",
|
||||
studyId: trial?.experiment.studyId,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Failed to initialize robot");
|
||||
}
|
||||
|
||||
toast.success("Robot initialized", {
|
||||
description: "Autonomous Life disabled and robot awake.",
|
||||
});
|
||||
},
|
||||
onError: (error: any) => {
|
||||
} catch (error: any) {
|
||||
toast.error("Robot initialization failed", {
|
||||
description: error.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
setIsInitializing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Log robot action mutation (for client-side execution)
|
||||
const logRobotActionMutation = api.trials.logRobotAction.useMutation({
|
||||
@@ -188,8 +205,34 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
},
|
||||
});
|
||||
|
||||
const executeSystemActionMutation =
|
||||
api.robots.plugins.executeSystemAction.useMutation();
|
||||
const executeSystemAction = async (
|
||||
actionId: string,
|
||||
params?: Record<string, unknown>,
|
||||
) => {
|
||||
setIsExecutingAction(true);
|
||||
try {
|
||||
const response = await fetch("/api/robots/command", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
action: "executeSystemAction",
|
||||
studyId: trial?.experiment.studyId,
|
||||
parameters: { id: actionId, parameters: params },
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || "Failed to execute action");
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
setIsExecutingAction(false);
|
||||
}
|
||||
};
|
||||
const [isCompleting, setIsCompleting] = useState(false);
|
||||
|
||||
// Map database step types to component step types
|
||||
@@ -236,10 +279,7 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
autoConnect: true,
|
||||
onSystemAction: async (actionId, parameters) => {
|
||||
console.log(`[Wizard] Executing system action: ${actionId}`, parameters);
|
||||
await executeSystemActionMutation.mutateAsync({
|
||||
id: actionId,
|
||||
parameters,
|
||||
});
|
||||
await executeSystemAction(actionId, parameters);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -549,7 +589,7 @@ export const WizardInterface = React.memo(function WizardInterface({
|
||||
"[WizardInterface] Triggering robot initialization:",
|
||||
trial.experiment.robotId,
|
||||
);
|
||||
initializeRobotMutation.mutate({ id: trial.experiment.robotId });
|
||||
await initializeRobot();
|
||||
}
|
||||
|
||||
toast.success("Trial started successfully");
|
||||
|
||||
@@ -7,7 +7,7 @@ import { experimentsRouter } from "~/server/api/routers/experiments";
|
||||
import { filesRouter } from "~/server/api/routers/files";
|
||||
import { mediaRouter } from "~/server/api/routers/media";
|
||||
import { participantsRouter } from "~/server/api/routers/participants";
|
||||
import { robotsRouter } from "~/server/api/routers/robots";
|
||||
import { pluginsRouter } from "~/server/api/routers/plugins";
|
||||
import { studiesRouter } from "~/server/api/routers/studies";
|
||||
import { trialsRouter } from "~/server/api/routers/trials";
|
||||
import { usersRouter } from "~/server/api/routers/users";
|
||||
@@ -26,9 +26,9 @@ export const appRouter = createTRPCRouter({
|
||||
experiments: experimentsRouter,
|
||||
participants: participantsRouter,
|
||||
trials: trialsRouter,
|
||||
robots: robotsRouter,
|
||||
files: filesRouter,
|
||||
media: mediaRouter,
|
||||
plugins: pluginsRouter,
|
||||
analytics: analyticsRouter,
|
||||
collaboration: collaborationRouter,
|
||||
admin: adminRouter,
|
||||
|
||||
235
src/server/api/routers/plugins.ts
Normal file
235
src/server/api/routers/plugins.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, type SQL } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||
import {
|
||||
pluginStatusEnum,
|
||||
plugins,
|
||||
studyMembers,
|
||||
studyPlugins,
|
||||
} from "~/server/db/schema";
|
||||
|
||||
export const pluginsRouter = createTRPCRouter({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
robotId: z.string().optional(),
|
||||
status: z.enum(pluginStatusEnum.enumValues).optional(),
|
||||
limit: z.number().min(1).max(100).default(50),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const conditions: SQL[] = [];
|
||||
|
||||
if (input.robotId) {
|
||||
conditions.push(eq(plugins.robotId, input.robotId));
|
||||
}
|
||||
|
||||
if (input.status) {
|
||||
conditions.push(eq(plugins.status, input.status));
|
||||
}
|
||||
|
||||
const query = ctx.db
|
||||
.select({
|
||||
id: plugins.id,
|
||||
robotId: plugins.robotId,
|
||||
name: plugins.name,
|
||||
version: plugins.version,
|
||||
description: plugins.description,
|
||||
author: plugins.author,
|
||||
repositoryUrl: plugins.repositoryUrl,
|
||||
trustLevel: plugins.trustLevel,
|
||||
status: plugins.status,
|
||||
createdAt: plugins.createdAt,
|
||||
updatedAt: plugins.updatedAt,
|
||||
metadata: plugins.metadata,
|
||||
})
|
||||
.from(plugins);
|
||||
|
||||
const results = await (
|
||||
conditions.length > 0 ? query.where(and(...conditions)) : query
|
||||
)
|
||||
.orderBy(desc(plugins.updatedAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset);
|
||||
|
||||
return results;
|
||||
}),
|
||||
|
||||
get: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const pluginResults = await ctx.db
|
||||
.select()
|
||||
.from(plugins)
|
||||
.where(eq(plugins.id, input.id))
|
||||
.limit(1);
|
||||
|
||||
const plugin = pluginResults[0];
|
||||
|
||||
if (!plugin) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Plugin not found",
|
||||
});
|
||||
}
|
||||
|
||||
return plugin;
|
||||
}),
|
||||
|
||||
install: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
studyId: z.string(),
|
||||
pluginId: z.string(),
|
||||
configuration: z.any().optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.session.user.id;
|
||||
|
||||
// Check if user has appropriate access
|
||||
const membership = await ctx.db.query.studyMembers.findFirst({
|
||||
where: and(
|
||||
eq(studyMembers.studyId, input.studyId),
|
||||
eq(studyMembers.userId, userId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!membership || !["owner", "researcher"].includes(membership.role)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Insufficient permissions to install plugins",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if plugin exists
|
||||
const plugin = await ctx.db
|
||||
.select()
|
||||
.from(plugins)
|
||||
.where(eq(plugins.id, input.pluginId))
|
||||
.limit(1);
|
||||
|
||||
if (!plugin[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Plugin not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Check if plugin is already installed
|
||||
const existing = await ctx.db
|
||||
.select()
|
||||
.from(studyPlugins)
|
||||
.where(
|
||||
and(
|
||||
eq(studyPlugins.studyId, input.studyId),
|
||||
eq(studyPlugins.pluginId, input.pluginId),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existing[0]) {
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "Plugin already installed for this study",
|
||||
});
|
||||
}
|
||||
|
||||
const installations = await ctx.db
|
||||
.insert(studyPlugins)
|
||||
.values({
|
||||
studyId: input.studyId,
|
||||
pluginId: input.pluginId,
|
||||
configuration: input.configuration ?? {},
|
||||
installedBy: userId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
const installation = installations[0];
|
||||
if (!installation) {
|
||||
throw new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to install plugin",
|
||||
});
|
||||
}
|
||||
|
||||
return installation;
|
||||
}),
|
||||
|
||||
uninstall: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
studyId: z.string(),
|
||||
pluginId: z.string(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const userId = ctx.session.user.id;
|
||||
|
||||
// Check if user has appropriate access
|
||||
const membership = await ctx.db.query.studyMembers.findFirst({
|
||||
where: and(
|
||||
eq(studyMembers.studyId, input.studyId),
|
||||
eq(studyMembers.userId, userId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!membership || !["owner", "researcher"].includes(membership.role)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "Insufficient permissions to uninstall plugins",
|
||||
});
|
||||
}
|
||||
|
||||
const result = await ctx.db
|
||||
.delete(studyPlugins)
|
||||
.where(
|
||||
and(
|
||||
eq(studyPlugins.studyId, input.studyId),
|
||||
eq(studyPlugins.pluginId, input.pluginId),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
|
||||
if (!result[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Plugin installation not found",
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}),
|
||||
|
||||
getActions: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
pluginId: z.string(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const plugin = await ctx.db
|
||||
.select({
|
||||
id: plugins.id,
|
||||
actionDefinitions: plugins.actionDefinitions,
|
||||
})
|
||||
.from(plugins)
|
||||
.where(eq(plugins.id, input.pluginId))
|
||||
.limit(1);
|
||||
|
||||
if (!plugin[0]) {
|
||||
throw new TRPCError({
|
||||
code: "NOT_FOUND",
|
||||
message: "Plugin not found",
|
||||
});
|
||||
}
|
||||
|
||||
return plugin[0].actionDefinitions ?? [];
|
||||
}),
|
||||
});
|
||||
@@ -841,6 +841,63 @@ export const studiesRouter = createTRPCRouter({
|
||||
};
|
||||
}),
|
||||
|
||||
getStudyPlugins: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
studyId: z.string().uuid(),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { studyId } = input;
|
||||
const userId = ctx.session.user.id;
|
||||
|
||||
// Check if user has access to this study (any role)
|
||||
const membership = await ctx.db.query.studyMembers.findFirst({
|
||||
where: and(
|
||||
eq(studyMembers.studyId, studyId),
|
||||
eq(studyMembers.userId, userId),
|
||||
),
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: "You don't have access to this study",
|
||||
});
|
||||
}
|
||||
|
||||
const installedPlugins = await ctx.db
|
||||
.select({
|
||||
plugin: {
|
||||
id: plugins.id,
|
||||
robotId: plugins.robotId,
|
||||
name: plugins.name,
|
||||
version: plugins.version,
|
||||
description: plugins.description,
|
||||
author: plugins.author,
|
||||
repositoryUrl: plugins.repositoryUrl,
|
||||
trustLevel: plugins.trustLevel,
|
||||
status: plugins.status,
|
||||
actionDefinitions: plugins.actionDefinitions,
|
||||
createdAt: plugins.createdAt,
|
||||
updatedAt: plugins.updatedAt,
|
||||
metadata: plugins.metadata,
|
||||
},
|
||||
installation: {
|
||||
id: studyPlugins.id,
|
||||
configuration: studyPlugins.configuration,
|
||||
installedAt: studyPlugins.installedAt,
|
||||
installedBy: studyPlugins.installedBy,
|
||||
},
|
||||
})
|
||||
.from(studyPlugins)
|
||||
.innerJoin(plugins, eq(studyPlugins.pluginId, plugins.id))
|
||||
.where(eq(studyPlugins.studyId, studyId))
|
||||
.orderBy(desc(studyPlugins.installedAt));
|
||||
|
||||
return installedPlugins;
|
||||
}),
|
||||
|
||||
// Plugin configuration management
|
||||
getPluginConfiguration: protectedProcedure
|
||||
.input(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||
import { useState } from "react";
|
||||
import SuperJSON from "superjson";
|
||||
|
||||
import { type AppRouter } from "~/server/api/root";
|
||||
import type { AppRouter } from "~/server/api/root";
|
||||
import { createQueryClient } from "./query-client";
|
||||
|
||||
let clientQueryClientSingleton: QueryClient | undefined = undefined;
|
||||
|
||||
Reference in New Issue
Block a user