mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 04:02:18 +01:00
Add application edit actions
This commit is contained in:
1 parent
401d82f6c4
commit
fb4d7cf221
5 files changed
+400
-16
No files matched your search
@@ -53,11 +53,11 @@ function mockToolsList(tools: unknown[]) {
|
||||
} as Response);
|
||||
}
|
||||
|
||||
function createRouteApp(db: ReturnType<typeof createDb>) {
|
||||
function createRouteApp(db: ReturnType<typeof createDb>, actor?: Express.Request["actor"]) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use((req, _res, next) => {
|
||||
req.actor = {
|
||||
req.actor = actor ?? {
|
||||
type: "board",
|
||||
userId: "board-user",
|
||||
userName: "Board User",
|
||||
@@ -470,6 +470,90 @@ describeEmbeddedPostgres("tool access service", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("updates tool applications through the board route and records activity", async () => {
|
||||
const company = await createCompany(db);
|
||||
const service = toolAccessService(db);
|
||||
const app = createRouteApp(db);
|
||||
const application = await service.createApplication(company.id, {
|
||||
name: "Editable app",
|
||||
description: "Before",
|
||||
type: "mcp_http",
|
||||
});
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/tool-applications/${application.id}`)
|
||||
.send({ name: "Edited app", description: "After" });
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toMatchObject({
|
||||
id: application.id,
|
||||
companyId: company.id,
|
||||
name: "Edited app",
|
||||
description: "After",
|
||||
type: "mcp_http",
|
||||
});
|
||||
const activities = await db.select().from(activityLog).where(eq(activityLog.entityId, application.id));
|
||||
expect(activities).toEqual([
|
||||
expect.objectContaining({
|
||||
action: "tool_application.updated",
|
||||
companyId: company.id,
|
||||
details: expect.objectContaining({ name: "Edited app" }),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns 409 instead of 500 when an application update collides with a duplicate name", async () => {
|
||||
const company = await createCompany(db);
|
||||
const service = toolAccessService(db);
|
||||
const app = createRouteApp(db);
|
||||
await service.createApplication(company.id, { name: "Existing app", type: "mcp_http" });
|
||||
const application = await service.createApplication(company.id, { name: "Editable app", type: "mcp_http" });
|
||||
|
||||
const res = await request(app)
|
||||
.patch(`/api/tool-applications/${application.id}`)
|
||||
.send({ name: "Existing app" });
|
||||
|
||||
expect(res.status).toBe(409);
|
||||
expect(res.body).toMatchObject({
|
||||
error: "A tool access record with that name already exists",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns 403 for cross-company application updates and 404 for missing applications", async () => {
|
||||
const allowedCompany = await createCompany(db);
|
||||
const otherCompany = await createCompany(db);
|
||||
const application = await toolAccessService(db).createApplication(otherCompany.id, {
|
||||
name: "Other company app",
|
||||
type: "mcp_http",
|
||||
});
|
||||
const app = createRouteApp(db, {
|
||||
type: "board",
|
||||
userId: "member-user",
|
||||
userName: "Member User",
|
||||
userEmail: null,
|
||||
companyIds: [allowedCompany.id],
|
||||
memberships: [
|
||||
{
|
||||
companyId: allowedCompany.id,
|
||||
membershipRole: "owner",
|
||||
status: "active",
|
||||
},
|
||||
],
|
||||
isInstanceAdmin: false,
|
||||
source: "session",
|
||||
});
|
||||
|
||||
const forbiddenRes = await request(app)
|
||||
.patch(`/api/tool-applications/${application.id}`)
|
||||
.send({ name: "Forbidden edit" });
|
||||
const missingRes = await request(createRouteApp(db))
|
||||
.patch(`/api/tool-applications/${randomUUID()}`)
|
||||
.send({ name: "Missing edit" });
|
||||
|
||||
expect(forbiddenRes.status).toBe(403);
|
||||
expect(missingRes.status).toBe(404);
|
||||
});
|
||||
|
||||
it("links run tool decisions to invocations, audit events, and pending action requests", async () => {
|
||||
const company = await createCompany(db);
|
||||
const [agent] = await db.insert(agents).values({
|
||||
|
||||
@@ -127,17 +127,21 @@ export function toolAccessRoutes(
|
||||
assertBoard(req);
|
||||
const existing = await svc.getApplication(req.params.applicationId as string);
|
||||
assertCompanyAccess(req, existing.companyId);
|
||||
const application = await svc.updateApplication(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
companyId: application.companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "tool_application.updated",
|
||||
entityType: "tool_application",
|
||||
entityId: application.id,
|
||||
details: { status: application.status, name: application.name },
|
||||
});
|
||||
res.json(application);
|
||||
try {
|
||||
const application = await svc.updateApplication(existing.id, req.body);
|
||||
await logActivity(db, {
|
||||
companyId: application.companyId,
|
||||
actorType: "user",
|
||||
actorId: req.actor.userId ?? "board",
|
||||
action: "tool_application.updated",
|
||||
entityType: "tool_application",
|
||||
entityId: application.id,
|
||||
details: { status: application.status, name: application.name },
|
||||
});
|
||||
res.json(application);
|
||||
} catch (error) {
|
||||
svc.ensureNoDuplicateNameError(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.get("/companies/:companyId/tools/connections", async (req, res) => {
|
||||
|
||||
@@ -2431,7 +2431,36 @@ export function toolAccessService(db: Db, options: ToolAccessServiceOptions = {}
|
||||
},
|
||||
|
||||
ensureNoDuplicateNameError: (error: unknown) => {
|
||||
if (error instanceof Error && /duplicate key value/.test(error.message)) {
|
||||
const maybeRecord = typeof error === "object" && error !== null ? error as Record<string, unknown> : null;
|
||||
const cause = maybeRecord?.cause;
|
||||
const maybeCause = typeof cause === "object" && cause !== null ? cause as Record<string, unknown> : null;
|
||||
const message = [
|
||||
error instanceof Error ? error.message : String(error),
|
||||
maybeRecord && typeof maybeRecord.detail === "string" ? maybeRecord.detail : null,
|
||||
maybeCause instanceof Error ? maybeCause.message : null,
|
||||
maybeCause && typeof maybeCause.detail === "string" ? maybeCause.detail : null,
|
||||
].filter(Boolean).join("\n");
|
||||
const code =
|
||||
maybeRecord && typeof maybeRecord.code === "string"
|
||||
? maybeRecord.code
|
||||
: maybeCause && typeof maybeCause.code === "string"
|
||||
? maybeCause.code
|
||||
: null;
|
||||
const constraint =
|
||||
maybeRecord && typeof maybeRecord.constraint === "string"
|
||||
? maybeRecord.constraint
|
||||
: maybeRecord && typeof maybeRecord.constraint_name === "string"
|
||||
? maybeRecord.constraint_name
|
||||
: maybeCause && typeof maybeCause.constraint === "string"
|
||||
? maybeCause.constraint
|
||||
: maybeCause && typeof maybeCause.constraint_name === "string"
|
||||
? maybeCause.constraint_name
|
||||
: null;
|
||||
if (
|
||||
code === "23505" ||
|
||||
constraint?.includes("tool_applications") ||
|
||||
/duplicate key value|unique constraint|tool_applications_company_id_name_unique/i.test(message)
|
||||
) {
|
||||
throw conflict("A tool access record with that name already exists");
|
||||
}
|
||||
throw error;
|
||||
|
||||
Reference in new issue
Block a user