mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Activating an automation with obviously-broken config (add_tag step
with empty tag_id, send_message with empty text, keyword_match
trigger with no keywords, etc.) used to succeed — and then every
dispatch produced a cryptic failed log row that users often missed.
- New validate.ts centralises the checks (mirrors engine.ts's
runtime guards one layer earlier so failures surface at save time).
Returns { path, message } issues so the UI can point users at the
exact problem.
- POST /api/automations rejects with 400 when is_active === true and
the merged config fails validation.
- PATCH /api/automations/[id] computes the post-patch "effective"
state (merging body fields over the existing row, loading current
steps if body.steps isn't provided) and applies the same gate.
Drafts (is_active=false) remain free to be incomplete so users can
still save progress mid-build.
- Builder surfaces the first issue in a toast with path as the
description, so users don't need DevTools to find what to fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { createClient } from '@/lib/supabase/server'
|
|
import { supabaseAdmin } from '@/lib/automations/admin-client'
|
|
import {
|
|
loadStepsTree,
|
|
replaceSteps,
|
|
type BuilderStepInput,
|
|
} from '@/lib/automations/steps-tree'
|
|
import {
|
|
validateStepsForActivation,
|
|
validateTriggerForActivation,
|
|
} from '@/lib/automations/validate'
|
|
|
|
async function requireUser() {
|
|
const supabase = await createClient()
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser()
|
|
return user
|
|
}
|
|
|
|
export async function GET(
|
|
_request: Request,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const { id } = await params
|
|
const user = await requireUser()
|
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
|
|
const admin = supabaseAdmin()
|
|
const { data: automation, error } = await admin
|
|
.from('automations')
|
|
.select('*')
|
|
.eq('id', id)
|
|
.eq('user_id', user.id)
|
|
.maybeSingle()
|
|
|
|
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
|
if (!automation) return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
|
|
const steps = await loadStepsTree(id)
|
|
return NextResponse.json({ automation, steps })
|
|
}
|
|
|
|
export async function PATCH(
|
|
request: Request,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const { id } = await params
|
|
const user = await requireUser()
|
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
|
|
const body = await request.json().catch(() => null)
|
|
if (!body) return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 })
|
|
|
|
const admin = supabaseAdmin()
|
|
|
|
// Ownership check before we touch anything. Load the fields we need
|
|
// to compute the post-patch "effective" state for validation.
|
|
const { data: existing } = await admin
|
|
.from('automations')
|
|
.select('id, user_id, is_active, trigger_type, trigger_config')
|
|
.eq('id', id)
|
|
.maybeSingle()
|
|
if (!existing || existing.user_id !== user.id) {
|
|
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
}
|
|
|
|
const update: Record<string, unknown> = {}
|
|
for (const k of [
|
|
'name',
|
|
'description',
|
|
'trigger_type',
|
|
'trigger_config',
|
|
'is_active',
|
|
] as const) {
|
|
if (k in body) update[k] = body[k]
|
|
}
|
|
|
|
// If this PATCH leaves the automation active (either explicitly
|
|
// activating it OR editing an already-active one), validate the
|
|
// merged configuration first. Activation is the natural gate — drafts
|
|
// are still allowed to be incomplete.
|
|
const willBeActive =
|
|
typeof update.is_active === 'boolean' ? update.is_active : existing.is_active
|
|
if (willBeActive) {
|
|
const mergedTriggerType = (update.trigger_type ?? existing.trigger_type) as string
|
|
const mergedTriggerConfig = update.trigger_config ?? existing.trigger_config
|
|
const mergedSteps = Array.isArray(body.steps)
|
|
? (body.steps as { step_type: string; step_config: Record<string, unknown> }[])
|
|
: await loadStepsTree(id)
|
|
const issues = [
|
|
...validateTriggerForActivation(mergedTriggerType, mergedTriggerConfig),
|
|
...validateStepsForActivation(mergedSteps),
|
|
]
|
|
if (issues.length > 0) {
|
|
return NextResponse.json(
|
|
{
|
|
error: 'Cannot keep automation active with invalid configuration',
|
|
issues,
|
|
},
|
|
{ status: 400 },
|
|
)
|
|
}
|
|
}
|
|
|
|
if (Object.keys(update).length > 0) {
|
|
const { error: updErr } = await admin
|
|
.from('automations')
|
|
.update(update)
|
|
.eq('id', id)
|
|
if (updErr) return NextResponse.json({ error: updErr.message }, { status: 500 })
|
|
}
|
|
|
|
if (Array.isArray(body.steps)) {
|
|
const err = await replaceSteps(id, body.steps as BuilderStepInput[])
|
|
if (err) return NextResponse.json({ error: err }, { status: 500 })
|
|
}
|
|
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
export async function DELETE(
|
|
_request: Request,
|
|
{ params }: { params: Promise<{ id: string }> },
|
|
) {
|
|
const { id } = await params
|
|
const user = await requireUser()
|
|
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
|
|
const { error } = await supabaseAdmin()
|
|
.from('automations')
|
|
.delete()
|
|
.eq('id', id)
|
|
.eq('user_id', user.id)
|
|
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
|
|
return NextResponse.json({ ok: true })
|
|
}
|