Files
patchy631--ai-engineering-hub/open-agent-builder/convex/approvals.ts
T
2026-07-13 12:37:47 +08:00

205 lines
5.2 KiB
TypeScript

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";
/**
* Convex Approvals - Human-in-the-loop workflow approvals
*
* Features:
* - Real-time subscriptions (no polling needed!)
* - Automatic reactivity when approval status changes
* - Perfect for pausing workflows waiting for human input
*/
// Create a new approval request
export const create = mutation({
args: {
approvalId: v.string(),
workflowId: v.id("workflows"),
nodeId: v.optional(v.string()),
executionId: v.optional(v.string()),
message: v.string(),
userId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const now = new Date().toISOString();
const approvalDoc = await ctx.db.insert("approvals", {
approvalId: args.approvalId,
workflowId: args.workflowId,
executionId: args.executionId,
nodeId: args.nodeId,
message: args.message,
status: "pending",
userId: args.userId,
createdAt: now,
});
return approvalDoc;
},
});
// Get approval by approvalId (for polling or one-time checks)
export const getByApprovalId = query({
args: { approvalId: v.string() },
handler: async (ctx, args) => {
const approval = await ctx.db
.query("approvals")
.withIndex("by_approvalId", (q) => q.eq("approvalId", args.approvalId))
.first();
return approval;
},
});
// Watch approval status - REAL-TIME SUBSCRIPTION
// Use this in your frontend to automatically resume when approved
export const watchStatus = query({
args: { approvalId: v.string() },
handler: async (ctx, args) => {
const approval = await ctx.db
.query("approvals")
.withIndex("by_approvalId", (q) => q.eq("approvalId", args.approvalId))
.first();
if (!approval) {
return { status: "not_found", approval: null };
}
return {
status: approval.status,
approval,
};
},
});
// Approve an approval request
export const approve = mutation({
args: {
approvalId: v.string(),
userId: v.optional(v.string()),
},
handler: async (ctx, args) => {
const approval = await ctx.db
.query("approvals")
.withIndex("by_approvalId", (q) => q.eq("approvalId", args.approvalId))
.first();
if (!approval) {
throw new Error(`Approval ${args.approvalId} not found`);
}
if (approval.status !== "pending") {
throw new Error(`Approval ${args.approvalId} already ${approval.status}`);
}
const now = new Date().toISOString();
await ctx.db.patch(approval._id, {
status: "approved",
respondedAt: now,
respondedBy: args.userId,
});
return { success: true, status: "approved" };
},
});
// Reject an approval request
export const reject = mutation({
args: {
approvalId: v.string(),
userId: v.optional(v.string()),
reason: v.optional(v.string()),
},
handler: async (ctx, args) => {
const approval = await ctx.db
.query("approvals")
.withIndex("by_approvalId", (q) => q.eq("approvalId", args.approvalId))
.first();
if (!approval) {
throw new Error(`Approval ${args.approvalId} not found`);
}
if (approval.status !== "pending") {
throw new Error(`Approval ${args.approvalId} already ${approval.status}`);
}
const now = new Date().toISOString();
await ctx.db.patch(approval._id, {
status: "rejected",
respondedAt: now,
respondedBy: args.userId,
});
return { success: true, status: "rejected", reason: args.reason };
},
});
// List pending approvals for a workflow
export const listPending = query({
args: { workflowId: v.id("workflows") },
handler: async (ctx, args) => {
const approvals = await ctx.db
.query("approvals")
.withIndex("by_workflow", (q) => q.eq("workflowId", args.workflowId))
.filter((q) => q.eq(q.field("status"), "pending"))
.collect();
return approvals;
},
});
// List all approvals for a workflow
export const listByWorkflow = query({
args: { workflowId: v.id("workflows") },
handler: async (ctx, args) => {
const approvals = await ctx.db
.query("approvals")
.withIndex("by_workflow", (q) => q.eq("workflowId", args.workflowId))
.order("desc")
.collect();
return approvals;
},
});
// Get approvals for a specific execution (useful for tracking what's blocking a run)
export const getByExecution = query({
args: { executionId: v.string() },
handler: async (ctx, args) => {
const approvals = await ctx.db
.query("approvals")
.withIndex("by_execution", (q) => q.eq("executionId", args.executionId))
.order("desc")
.collect();
return approvals;
},
});
// Clean up old approvals (approved/rejected older than 24 hours)
export const cleanup = mutation({
args: {},
handler: async (ctx) => {
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const oldApprovals = await ctx.db
.query("approvals")
.filter((q) =>
q.and(
q.neq(q.field("status"), "pending"),
q.lt(q.field("createdAt"), oneDayAgo)
)
)
.collect();
for (const approval of oldApprovals) {
await ctx.db.delete(approval._id);
}
return { deleted: oldApprovals.length };
},
});