feat: Function triggers — schedule, inbound hook, domain events

Migration 00016: function_id on scheduled_jobs/inbound_hooks,
event_pattern on functions, 'event' in the run-trigger enum.

- Jobs deliver to a function instead of HTTP when function_id is set;
  the run's status_code mirrors the process exit code. Clearing the
  field returns the job to URL delivery.
- Inbound hooks gain mode "function" — received body becomes stdin
  payload, acked immediately, run recorded async.
- event_pattern (document.*, *, exact type) fires the function detached
  on matching publishEvent — never blocks realtime/webhook fan-out.
- One executeFunction path serves manual/schedule/hook/event; run rows
  record the trigger source.
- Backend image now ships bun 1.3.14 so Functions work under compose.

Dashboard: job target picker, hook function mode, event-pattern field.

Verified live: job run → success (status_code 0), hook POST → run,
document.created → event run with correct payload.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-24 20:28:04 +02:00
1 parent c094afad65
commit a84049b8fc
40 files changed
+716 -157

No files matched your search

+25 -4
View File
@@ -4,11 +4,13 @@ import {
AutomationService,
CreateInboundHookRequest,
CreateIntegrationRequest,
FunctionsService,
InboundService,
IntegrationsService,
UpsertAlertRuleRequest,
WebhooksService,
type AlertRule,
type Function as PrimoraFunction,
type InboundHook,
type ScheduledJob,
type Integration,
@@ -74,7 +76,8 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
const [showCreateWebhook, setShowCreateWebhook] = createSignal(false);
const [showCreateAlert, setShowCreateAlert] = createSignal(false);
const [showCreateInbound, setShowCreateInbound] = createSignal(false);
const [inboundForm, setInboundForm] = createSignal({ name: "", mode: CreateInboundHookRequest.mode.EVENT as CreateInboundHookRequest.mode, job_id: "", secret: "" });
const [inboundForm, setInboundForm] = createSignal({ name: "", mode: CreateInboundHookRequest.mode.EVENT as CreateInboundHookRequest.mode, job_id: "", function_id: "", secret: "" });
const [functions, setFunctions] = createSignal<PrimoraFunction[]>([]);
const [copiedUrl, setCopiedUrl] = createSignal("");
const [alertForm, setAlertForm] = createSignal({
name: "",
@@ -97,18 +100,20 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
setLoading(true);
setError("");
try {
const [int, hooks, rules, inbound, jobs] = await Promise.all([
const [int, hooks, rules, inbound, jobs, fns] = await Promise.all([
intSvc(props.demoMode).listIntegrations({ projectId: props.projectID }),
hookSvc(props.demoMode).listWebhooks({ projectId: props.projectID }),
props.demoMode ? Promise.resolve({ items: [] }) : AlertsService.listAlertRules({ projectId: props.projectID }),
props.demoMode ? Promise.resolve({ items: [] }) : InboundService.listInboundHooks({ projectId: props.projectID }),
props.demoMode ? Promise.resolve({ items: [] }) : AutomationService.listScheduledJobs({ projectId: props.projectID }),
props.demoMode ? Promise.resolve({ items: [] }) : FunctionsService.listFunctions({ projectId: props.projectID }),
]);
setIntegrations(int.items ?? []);
setWebhooks(hooks.items ?? []);
setAlertRules(rules.items ?? []);
setInboundHooks(inbound.items ?? []);
setScheduledJobs(jobs.items ?? []);
setFunctions(fns.items ?? []);
} catch (e) {
setError(err(e));
} finally {
@@ -268,11 +273,12 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
name: form.name.trim(),
mode: form.mode,
job_id: form.mode === CreateInboundHookRequest.mode.JOB ? form.job_id : undefined,
function_id: form.mode === CreateInboundHookRequest.mode.FUNCTION ? form.function_id : undefined,
secret: form.secret.trim() || undefined,
},
});
setShowCreateInbound(false);
setInboundForm({ name: "", mode: CreateInboundHookRequest.mode.EVENT, job_id: "", secret: "" });
setInboundForm({ name: "", mode: CreateInboundHookRequest.mode.EVENT, job_id: "", function_id: "", secret: "" });
}, "Inbound hook created");
};
@@ -706,7 +712,9 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
<tr>
<td class="font-medium">{hook.name}</td>
<td>
<Badge variant="neutral">{hook.mode === "job" ? "job trigger" : "event"}</Badge>
<Badge variant="neutral">
{hook.mode === "job" ? "job trigger" : hook.mode === "function" ? `ƒ ${functions().find((f) => f.id === hook.function_id)?.name ?? "function"}` : "event"}
</Badge>
<Show when={!hook.enabled}>
{" "}<Badge variant="warning">disabled</Badge>
</Show>
@@ -943,8 +951,21 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
options={[
{ value: "event", label: "Publish inbound.received event" },
{ value: "job", label: "Trigger a scheduled job" },
{ value: "function", label: "Invoke a function" },
]}
/>
<Show when={inboundForm().mode === CreateInboundHookRequest.mode.FUNCTION}>
<Select
label="Function"
value={inboundForm().function_id}
onChange={(e) => setInboundForm((c) => ({ ...c, function_id: e.currentTarget.value }))}
options={[
{ value: "", label: "Select a function…" },
...functions().map((f) => ({ value: f.id, label: f.name })),
]}
/>
<p class="text-text-2 text-xs">The received body becomes the function's stdin payload.</p>
</Show>
<Show when={inboundForm().mode === CreateInboundHookRequest.mode.JOB}>
<Select
label="Job"