Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions scripts/002_create_rls_policies.sql
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,32 @@ CREATE POLICY "Editors can create workflow versions"
)
);

DROP POLICY IF EXISTS "Editors can update workflow versions" ON public.workflow_versions;
CREATE POLICY "Editors can update workflow versions"
ON public.workflow_versions FOR UPDATE
USING (
workflow_id IN (
SELECT id FROM public.workflows
WHERE workspace_id IN (
SELECT workspace_id FROM public.get_user_workspace_roles()
WHERE role IN ('owner', 'editor')
)
)
);

DROP POLICY IF EXISTS "Editors can delete workflow versions" ON public.workflow_versions;
CREATE POLICY "Editors can delete workflow versions"
ON public.workflow_versions FOR DELETE
USING (
workflow_id IN (
SELECT id FROM public.workflows
WHERE workspace_id IN (
SELECT workspace_id FROM public.get_user_workspace_roles()
WHERE role IN ('owner', 'editor')
)
)
);

-- Workflow executions policies
DROP POLICY IF EXISTS "Users can view executions in their workspaces" ON public.workflow_executions;
CREATE POLICY "Users can view executions in their workspaces"
Expand Down
4 changes: 2 additions & 2 deletions src/app/api/workflows/[id]/auto-layout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
if (result.error) return result.error

const { id: workflowId } = await params
const workflow = await getWorkflow(workflowId)
const workflow = await getWorkflow(workflowId, result.workspace.id)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
}

const layoutedNodes = autoLayout.applyLayout(workflow.nodes, workflow.connections)
const updated = await updateWorkflow(workflowId, { nodes: layoutedNodes })
const updated = await updateWorkflow(workflowId, { nodes: layoutedNodes }, result.workspace.id)

return NextResponse.json(updated)
}
2 changes: 1 addition & 1 deletion src/app/api/workflows/[id]/connections/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const connectionData = await request.json()

try {
const workflow = await addWorkflowConnection(id, connectionData)
const workflow = await addWorkflowConnection(id, connectionData, result.workspace.id)
const newConnection = workflow.connections[workflow.connections.length - 1]
return NextResponse.json(newConnection)
} catch (err) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/workflows/[id]/copy/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const { id: workflowId } = await params
const { nodeIds } = await request.json()

const workflow = await getWorkflow(workflowId)
const workflow = await getWorkflow(workflowId, result.workspace.id)
if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
const { id } = await params
const { input } = await request.json()

const workflow = await getWorkflow(id)
const workflow = await getWorkflow(id, result.workspace.id)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/workflows/[id]/export/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
if (result.error) return result.error

const { id: workflowId } = await params
const workflow = await getWorkflow(workflowId)
const workflow = await getWorkflow(workflowId, result.workspace.id)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
Expand Down
18 changes: 9 additions & 9 deletions src/app/api/workflows/[id]/history/status/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Deprecated; use client HistoryManager for history status (Builder Hardening Sprint 0).
import { type NextRequest, NextResponse } from "next/server"
import { withWorkspace } from "@/lib/api/with-workspace"
import { getHistoryManager } from "@/lib/history-manager"

export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function GET(_request: NextRequest, _ctx: { params: Promise<{ id: string }> }) {
const result = await withWorkspace()
if (result.error) return result.error

const { id: workflowId } = await params
const historyManager = getHistoryManager(workflowId)

return NextResponse.json({
canUndo: historyManager.canUndo(),
canRedo: historyManager.canRedo(),
})
return NextResponse.json(
{
error:
"This history status endpoint is gone (410). Undo/redo/history status are client-only via HistoryManager (Builder Hardening Sprint 0).",
},
{ status: 410 },
)
}
4 changes: 2 additions & 2 deletions src/app/api/workflows/[id]/nodes/[nodeId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function PATCH(
const updates = await request.json()

try {
const workflow = await updateWorkflowNode(id, nodeId, updates)
const workflow = await updateWorkflowNode(id, nodeId, updates, result.workspace.id)
const node = workflow.nodes.find((n) => n.id === nodeId)
if (!node) return NextResponse.json({ error: "Node not found" }, { status: 404 })
return NextResponse.json(node)
Expand All @@ -32,7 +32,7 @@ export async function DELETE(
const { id, nodeId } = await params

try {
await deleteWorkflowNode(id, nodeId)
await deleteWorkflowNode(id, nodeId, result.workspace.id)
return NextResponse.json({ success: true })
} catch {
return NextResponse.json({ error: "Node not found" }, { status: 404 })
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/workflows/[id]/nodes/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
const nodeData = await request.json()

try {
const workflow = await addWorkflowNode(id, nodeData)
const workflow = await addWorkflowNode(id, nodeData, result.workspace.id)
const newNode = workflow.nodes[workflow.nodes.length - 1]
return NextResponse.json(newNode)
} catch (err: unknown) {
Expand Down
14 changes: 9 additions & 5 deletions src/app/api/workflows/[id]/paste/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Nothing to paste" }, { status: 400 })
}

const workflow = await getWorkflow(workflowId)
const workflow = await getWorkflow(workflowId, result.workspace.id)
if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
}
Expand Down Expand Up @@ -59,10 +59,14 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
...(pastedConnections as typeof workflow.connections),
]

const updated = await updateWorkflow(workflowId, {
nodes: updatedNodes,
connections: updatedConnections,
})
const updated = await updateWorkflow(
workflowId,
{
nodes: updatedNodes,
connections: updatedConnections,
},
result.workspace.id,
)

const newNodeIds = pastedNodes.map((n) => n.id)
return NextResponse.json({ success: true, nodeIds: newNodeIds, workflow: updated })
Expand Down
32 changes: 9 additions & 23 deletions src/app/api/workflows/[id]/redo/route.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
// Deprecated; use client HistoryManager for undo/redo (Builder Hardening Sprint 0).
import { type NextRequest, NextResponse } from "next/server"
import { withWorkspace } from "@/lib/api/with-workspace"
import { getWorkflow, updateWorkflow } from "@/lib/db/workflows"
import { getHistoryManager } from "@/lib/history-manager"

export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function POST(_request: NextRequest, _ctx: { params: Promise<{ id: string }> }) {
const result = await withWorkspace()
if (result.error) return result.error

const { id: workflowId } = await params
const workflow = await getWorkflow(workflowId)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
}

const historyManager = getHistoryManager(workflowId)
const nextWorkflow = historyManager.redo(workflow)

if (!nextWorkflow) {
return NextResponse.json({ error: "Nothing to redo" }, { status: 400 })
}

const updated = await updateWorkflow(workflowId, {
nodes: nextWorkflow.nodes,
connections: nextWorkflow.connections,
})

return NextResponse.json(updated)
return NextResponse.json(
{
error:
"This redo endpoint is gone (410). Undo/redo are client-only via HistoryManager (Builder Hardening Sprint 0).",
},
{ status: 410 },
)
}
6 changes: 3 additions & 3 deletions src/app/api/workflows/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
if (result.error) return result.error

const { id } = await params
const workflow = await getWorkflow(id)
const workflow = await getWorkflow(id, result.workspace.id)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
Expand All @@ -22,7 +22,7 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id

const { id } = await params
const updates = await request.json()
const workflow = await updateWorkflow(id, updates)
const workflow = await updateWorkflow(id, updates, result.workspace.id)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
Expand All @@ -36,7 +36,7 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
if (result.error) return result.error

const { id } = await params
const deleted = await deleteWorkflow(id)
const deleted = await deleteWorkflow(id, result.workspace.id)

if (!deleted) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
Expand Down
32 changes: 9 additions & 23 deletions src/app/api/workflows/[id]/undo/route.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,16 @@
// Deprecated; use client HistoryManager for undo/redo (Builder Hardening Sprint 0).
import { type NextRequest, NextResponse } from "next/server"
import { withWorkspace } from "@/lib/api/with-workspace"
import { getWorkflow, updateWorkflow } from "@/lib/db/workflows"
import { getHistoryManager } from "@/lib/history-manager"

export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
export async function POST(_request: NextRequest, _ctx: { params: Promise<{ id: string }> }) {
const result = await withWorkspace()
if (result.error) return result.error

const { id: workflowId } = await params
const workflow = await getWorkflow(workflowId)

if (!workflow) {
return NextResponse.json({ error: "Workflow not found" }, { status: 404 })
}

const historyManager = getHistoryManager(workflowId)
const previousWorkflow = historyManager.undo(workflow)

if (!previousWorkflow) {
return NextResponse.json({ error: "Nothing to undo" }, { status: 400 })
}

const updated = await updateWorkflow(workflowId, {
nodes: previousWorkflow.nodes,
connections: previousWorkflow.connections,
})

return NextResponse.json(updated)
return NextResponse.json(
{
error:
"This undo endpoint is gone (410). Undo/redo are client-only via HistoryManager (Builder Hardening Sprint 0).",
},
{ status: 410 },
)
}
86 changes: 66 additions & 20 deletions src/app/api/workflows/[id]/versions/[version]/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,27 @@
import { type NextRequest, NextResponse } from "next/server"
import { z } from "zod"
import { parseVersionParam } from "@/lib/api/version-param"
import { withWorkspace } from "@/lib/api/with-workspace"
import { versionStore } from "@/lib/version-store"
import { versionStore, type VersionWriteResult } from "@/lib/version-store"

const tagVersionBodySchema = z.object({
tag: z.string().min(1, "tag must be a non-empty string"),
})

function writeResultResponse(result: VersionWriteResult): NextResponse {
switch (result) {
case "ok":
return NextResponse.json({ success: true })
case "not_found":
return NextResponse.json({ error: "Version not found" }, { status: 404 })
case "forbidden":
return NextResponse.json({ error: "Forbidden" }, { status: 403 })
default: {
const _exhaustive: never = result
return _exhaustive
}
}
}

export async function GET(
request: NextRequest,
Expand All @@ -10,14 +31,24 @@ export async function GET(
if (result.error) return result.error

const { id, version } = await params
const versionNumber = Number.parseInt(version)
const versionData = versionStore.getVersion(id, versionNumber)

if (!versionData) {
return NextResponse.json({ error: "Version not found" }, { status: 404 })
const parsed = parseVersionParam(version)
if (!parsed.ok) {
return NextResponse.json(
{ error: `Invalid version number: expected integer, got ${JSON.stringify(version)}` },
{ status: 400 },
)
}

return NextResponse.json(versionData)
try {
const versionData = await versionStore.getVersion(id, parsed.value)
if (!versionData) {
return NextResponse.json({ error: "Version not found" }, { status: 404 })
}
return NextResponse.json(versionData)
} catch (error) {
console.error("Version get error:", error instanceof Error ? error.message : String(error))
return NextResponse.json({ error: "Failed to load version" }, { status: 500 })
}
}

export async function DELETE(
Expand All @@ -28,14 +59,21 @@ export async function DELETE(
if (result.error) return result.error

const { id, version } = await params
const versionNumber = Number.parseInt(version)
const success = versionStore.deleteVersion(id, versionNumber)

if (!success) {
return NextResponse.json({ error: "Version not found" }, { status: 404 })
const parsed = parseVersionParam(version)
if (!parsed.ok) {
return NextResponse.json(
{ error: `Invalid version number: expected integer, got ${JSON.stringify(version)}` },
{ status: 400 },
)
}

return NextResponse.json({ success: true })
try {
const writeResult = await versionStore.deleteVersion(id, parsed.value)
return writeResultResponse(writeResult)
} catch (error) {
console.error("Version delete error:", error instanceof Error ? error.message : String(error))
return NextResponse.json({ error: "Failed to delete version" }, { status: 500 })
}
}

export async function PATCH(
Expand All @@ -47,16 +85,24 @@ export async function PATCH(

try {
const { id, version } = await params
const { tag } = await request.json()
const versionNumber = Number.parseInt(version)
const success = versionStore.tagVersion(id, versionNumber, tag)

if (!success) {
return NextResponse.json({ error: "Version not found" }, { status: 404 })
const parsed = parseVersionParam(version)
if (!parsed.ok) {
return NextResponse.json(
{ error: `Invalid version number: expected integer, got ${JSON.stringify(version)}` },
{ status: 400 },
)
}

return NextResponse.json({ success: true })
const body = tagVersionBodySchema.parse(await request.json())
const writeResult = await versionStore.tagVersion(id, parsed.value, body.tag)
return writeResultResponse(writeResult)
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request body", details: error.flatten() },
{ status: 400 },
)
}
console.error("Version tag error:", error instanceof Error ? error.message : String(error))
return NextResponse.json({ error: "Failed to tag version" }, { status: 500 })
}
Expand Down
Loading
Loading