Implement execution workspaces and work products
This commit is contained in:
68
server/src/routes/execution-workspaces.ts
Normal file
68
server/src/routes/execution-workspaces.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { Router } from "express";
|
||||
import type { Db } from "@paperclipai/db";
|
||||
import { updateExecutionWorkspaceSchema } from "@paperclipai/shared";
|
||||
import { validate } from "../middleware/validate.js";
|
||||
import { executionWorkspaceService, logActivity } from "../services/index.js";
|
||||
import { assertCompanyAccess, getActorInfo } from "./authz.js";
|
||||
|
||||
export function executionWorkspaceRoutes(db: Db) {
|
||||
const router = Router();
|
||||
const svc = executionWorkspaceService(db);
|
||||
|
||||
router.get("/companies/:companyId/execution-workspaces", async (req, res) => {
|
||||
const companyId = req.params.companyId as string;
|
||||
assertCompanyAccess(req, companyId);
|
||||
const workspaces = await svc.list(companyId, {
|
||||
projectId: req.query.projectId as string | undefined,
|
||||
projectWorkspaceId: req.query.projectWorkspaceId as string | undefined,
|
||||
issueId: req.query.issueId as string | undefined,
|
||||
status: req.query.status as string | undefined,
|
||||
reuseEligible: req.query.reuseEligible === "true",
|
||||
});
|
||||
res.json(workspaces);
|
||||
});
|
||||
|
||||
router.get("/execution-workspaces/:id", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const workspace = await svc.getById(id);
|
||||
if (!workspace) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, workspace.companyId);
|
||||
res.json(workspace);
|
||||
});
|
||||
|
||||
router.patch("/execution-workspaces/:id", validate(updateExecutionWorkspaceSchema), async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const existing = await svc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
const workspace = await svc.update(id, {
|
||||
...req.body,
|
||||
...(req.body.cleanupEligibleAt ? { cleanupEligibleAt: new Date(req.body.cleanupEligibleAt) } : {}),
|
||||
});
|
||||
if (!workspace) {
|
||||
res.status(404).json({ error: "Execution workspace not found" });
|
||||
return;
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: existing.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "execution_workspace.updated",
|
||||
entityType: "execution_workspace",
|
||||
entityId: workspace.id,
|
||||
details: { changedKeys: Object.keys(req.body).sort() },
|
||||
});
|
||||
res.json(workspace);
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -4,10 +4,12 @@ import type { Db } from "@paperclipai/db";
|
||||
import {
|
||||
addIssueCommentSchema,
|
||||
createIssueAttachmentMetadataSchema,
|
||||
createIssueWorkProductSchema,
|
||||
createIssueLabelSchema,
|
||||
checkoutIssueSchema,
|
||||
createIssueSchema,
|
||||
linkIssueApprovalSchema,
|
||||
updateIssueWorkProductSchema,
|
||||
updateIssueSchema,
|
||||
} from "@paperclipai/shared";
|
||||
import type { StorageService } from "../storage/types.js";
|
||||
@@ -15,12 +17,14 @@ import { validate } from "../middleware/validate.js";
|
||||
import {
|
||||
accessService,
|
||||
agentService,
|
||||
executionWorkspaceService,
|
||||
goalService,
|
||||
heartbeatService,
|
||||
issueApprovalService,
|
||||
issueService,
|
||||
logActivity,
|
||||
projectService,
|
||||
workProductService,
|
||||
} from "../services/index.js";
|
||||
import { logger } from "../middleware/logger.js";
|
||||
import { forbidden, HttpError, unauthorized } from "../errors.js";
|
||||
@@ -37,6 +41,8 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
const projectsSvc = projectService(db);
|
||||
const goalsSvc = goalService(db);
|
||||
const issueApprovalsSvc = issueApprovalService(db);
|
||||
const executionWorkspacesSvc = executionWorkspaceService(db);
|
||||
const workProductsSvc = workProductService(db);
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: MAX_ATTACHMENT_BYTES, files: 1 },
|
||||
@@ -304,6 +310,10 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
const mentionedProjects = mentionedProjectIds.length > 0
|
||||
? await projectsSvc.listByIds(issue.companyId, mentionedProjectIds)
|
||||
: [];
|
||||
const currentExecutionWorkspace = issue.executionWorkspaceId
|
||||
? await executionWorkspacesSvc.getById(issue.executionWorkspaceId)
|
||||
: null;
|
||||
const workProducts = await workProductsSvc.listForIssue(issue.id);
|
||||
res.json({
|
||||
...issue,
|
||||
goalId: goal?.id ?? issue.goalId,
|
||||
@@ -311,9 +321,110 @@ export function issueRoutes(db: Db, storage: StorageService) {
|
||||
project: project ?? null,
|
||||
goal: goal ?? null,
|
||||
mentionedProjects,
|
||||
currentExecutionWorkspace,
|
||||
workProducts,
|
||||
});
|
||||
});
|
||||
|
||||
router.get("/issues/:id/work-products", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await svc.getById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, issue.companyId);
|
||||
const workProducts = await workProductsSvc.listForIssue(issue.id);
|
||||
res.json(workProducts);
|
||||
});
|
||||
|
||||
router.post("/issues/:id/work-products", validate(createIssueWorkProductSchema), async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await svc.getById(id);
|
||||
if (!issue) {
|
||||
res.status(404).json({ error: "Issue not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, issue.companyId);
|
||||
const product = await workProductsSvc.createForIssue(issue.id, issue.companyId, {
|
||||
...req.body,
|
||||
projectId: req.body.projectId ?? issue.projectId ?? null,
|
||||
});
|
||||
if (!product) {
|
||||
res.status(422).json({ error: "Invalid work product payload" });
|
||||
return;
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: issue.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.work_product_created",
|
||||
entityType: "issue",
|
||||
entityId: issue.id,
|
||||
details: { workProductId: product.id, type: product.type, provider: product.provider },
|
||||
});
|
||||
res.status(201).json(product);
|
||||
});
|
||||
|
||||
router.patch("/work-products/:id", validate(updateIssueWorkProductSchema), async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const existing = await workProductsSvc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Work product not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
const product = await workProductsSvc.update(id, req.body);
|
||||
if (!product) {
|
||||
res.status(404).json({ error: "Work product not found" });
|
||||
return;
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: existing.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.work_product_updated",
|
||||
entityType: "issue",
|
||||
entityId: existing.issueId,
|
||||
details: { workProductId: product.id, changedKeys: Object.keys(req.body).sort() },
|
||||
});
|
||||
res.json(product);
|
||||
});
|
||||
|
||||
router.delete("/work-products/:id", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const existing = await workProductsSvc.getById(id);
|
||||
if (!existing) {
|
||||
res.status(404).json({ error: "Work product not found" });
|
||||
return;
|
||||
}
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
const removed = await workProductsSvc.remove(id);
|
||||
if (!removed) {
|
||||
res.status(404).json({ error: "Work product not found" });
|
||||
return;
|
||||
}
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
companyId: existing.companyId,
|
||||
actorType: actor.actorType,
|
||||
actorId: actor.actorId,
|
||||
agentId: actor.agentId,
|
||||
runId: actor.runId,
|
||||
action: "issue.work_product_deleted",
|
||||
entityType: "issue",
|
||||
entityId: existing.issueId,
|
||||
details: { workProductId: removed.id, type: removed.type },
|
||||
});
|
||||
res.json(removed);
|
||||
});
|
||||
|
||||
router.post("/issues/:id/read", async (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const issue = await svc.getById(id);
|
||||
|
||||
Reference in New Issue
Block a user