wacrm/src/app/api/automations/route.ts
Arnas Donauskas 570fc1234c
fix(automations): reject activation of invalid step/trigger config
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>
2026-04-17 21:50:13 +03:00

109 lines
3.6 KiB
TypeScript

import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase/server'
import { supabaseAdmin } from '@/lib/automations/admin-client'
import { getTemplate } from '@/lib/automations/templates'
import { insertSteps, type BuilderStepInput } from '@/lib/automations/steps-tree'
import {
validateStepsForActivation,
validateTriggerForActivation,
} from '@/lib/automations/validate'
export async function GET() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
const { data, error } = await supabase
.from('automations')
.select('*')
.order('created_at', { ascending: false })
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
return NextResponse.json({ automations: data ?? [] })
}
export async function POST(request: Request) {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
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 { name, description, trigger_type, trigger_config, is_active, steps, template } = body
let effectiveSteps: BuilderStepInput[] | undefined = steps
let effectiveName = name
let effectiveDescription = description
let effectiveTriggerType = trigger_type
let effectiveTriggerConfig = trigger_config
if (template && (!steps || steps.length === 0)) {
const t = getTemplate(template)
if (t) {
effectiveName = effectiveName ?? t.name
effectiveDescription = effectiveDescription ?? t.description
effectiveTriggerType = effectiveTriggerType ?? t.trigger_type
effectiveTriggerConfig = effectiveTriggerConfig ?? t.trigger_config
effectiveSteps = t.steps as unknown as BuilderStepInput[]
}
}
if (!effectiveName || !effectiveTriggerType) {
return NextResponse.json(
{ error: 'name and trigger_type are required' },
{ status: 400 },
)
}
// Block activation of a clearly broken automation up-front instead of
// letting every trigger silently produce a failed log row. Drafts
// (is_active=false) are allowed to be incomplete so users can save
// progress mid-build.
if (is_active) {
const issues = [
...validateTriggerForActivation(effectiveTriggerType, effectiveTriggerConfig ?? {}),
...validateStepsForActivation(
(effectiveSteps ?? []) as unknown as { step_type: string; step_config: Record<string, unknown> }[],
),
]
if (issues.length > 0) {
return NextResponse.json(
{ error: 'Cannot activate automation with invalid configuration', issues },
{ status: 400 },
)
}
}
const admin = supabaseAdmin()
const { data: automation, error: insertErr } = await admin
.from('automations')
.insert({
user_id: user.id,
name: effectiveName,
description: effectiveDescription ?? null,
trigger_type: effectiveTriggerType,
trigger_config: effectiveTriggerConfig ?? {},
is_active: !!is_active,
})
.select()
.single()
if (insertErr || !automation) {
return NextResponse.json(
{ error: insertErr?.message ?? 'insert failed' },
{ status: 500 },
)
}
if (effectiveSteps && effectiveSteps.length > 0) {
const err = await insertSteps(automation.id, effectiveSteps)
if (err) return NextResponse.json({ error: err }, { status: 500 })
}
return NextResponse.json({ automation }, { status: 201 })
}