mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 07:41:23 +00:00
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:
1 parent
c094afad65
commit
a84049b8fc
40 files changed
+716
-157
No files matched your search
@@ -25,6 +25,7 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
|
|||||||
- **Email templates + send log** (`core.email_log`, migration 00014) — the mailer renders registered templates (currently `invitation`) and every send — successful or failed — is recorded with template, recipient, subject, status, and error text. `GET /projects/:id/emails` lists recent sends; the Members page shows an "Email log" card under pending invitations.
|
- **Email templates + send log** (`core.email_log`, migration 00014) — the mailer renders registered templates (currently `invitation`) and every send — successful or failed — is recorded with template, recipient, subject, status, and error text. `GET /projects/:id/emails` lists recent sends; the Members page shows an "Email log" card under pending invitations.
|
||||||
- **S3-compatible object storage backend** — `BACKEND_STORAGE_DRIVER=s3` swaps the local disk store for any S3-compatible API (AWS, MinIO, Garage, R2) via a stdlib-only SigV4 implementation — no AWS SDK dependency. `S3_ENDPOINT`/`S3_REGION`/`S3_BUCKET`/credentials configure it; `S3_PATH_STYLE` (default on) covers MinIO/Garage, off for AWS virtual-hosted. The configured bucket is created on startup if missing; objects stream with computed SHA-256, `storage_path` records `s3://bucket/key`. Verified live end-to-end (bucket → put → get → copy → list → delete).
|
- **S3-compatible object storage backend** — `BACKEND_STORAGE_DRIVER=s3` swaps the local disk store for any S3-compatible API (AWS, MinIO, Garage, R2) via a stdlib-only SigV4 implementation — no AWS SDK dependency. `S3_ENDPOINT`/`S3_REGION`/`S3_BUCKET`/credentials configure it; `S3_PATH_STYLE` (default on) covers MinIO/Garage, off for AWS virtual-hosted. The configured bucket is created on startup if missing; objects stream with computed SHA-256, `storage_path` records `s3://bucket/key`. Verified live end-to-end (bucket → put → get → copy → list → delete).
|
||||||
- **Functions** (`core.functions` + `core.function_runs`, migration 00015) — project-scoped JS/TS executed by a runtime binary on the backend host (`bun` or `deno`, per function). The JSON payload arrives on stdin and as `PRIMORA_PAYLOAD`; stdout/stderr are captured per run (capped at 64 KiB) with status, exit code and duration — `FUNCTIONS_RUNTIME`/`FUNCTIONS_TIMEOUT_SECONDS`/`FUNCTIONS_MAX_OUTPUT_BYTES` configure the runner. API: CRUD under `/projects/:id/functions`, `POST …/invoke` (502 + recorded run on failure), `GET …/runs`. Dashboard gains a Functions page: code editor, JSON payload invoke, live result, run history. Code executes with backend privileges — the documented self-hosted trust model.
|
- **Functions** (`core.functions` + `core.function_runs`, migration 00015) — project-scoped JS/TS executed by a runtime binary on the backend host (`bun` or `deno`, per function). The JSON payload arrives on stdin and as `PRIMORA_PAYLOAD`; stdout/stderr are captured per run (capped at 64 KiB) with status, exit code and duration — `FUNCTIONS_RUNTIME`/`FUNCTIONS_TIMEOUT_SECONDS`/`FUNCTIONS_MAX_OUTPUT_BYTES` configure the runner. API: CRUD under `/projects/:id/functions`, `POST …/invoke` (502 + recorded run on failure), `GET …/runs`. Dashboard gains a Functions page: code editor, JSON payload invoke, live result, run history. Code executes with backend privileges — the documented self-hosted trust model.
|
||||||
|
- **Function triggers** (migration 00016) — three new ways to run a function, all through the same executor and run log. Scheduled jobs accept `function_id` instead of `url` (run status mirrors the exit code); inbound hooks gain `mode: "function"` — the received body becomes the function's stdin payload, acked immediately with the run recorded async; and `event_pattern` on a function (`document.*`, `*`, or an exact type) fires it detached whenever a matching domain event publishes — `{event, project_id, occurred_at, data}` as payload. Runs record trigger source: `manual`/`schedule`/`hook`/`event`. Dashboard: function event-trigger field, job target picker (URL or function), and inbound-hook function mode.
|
||||||
|
|
||||||
## [0.7.1] - 2026-09-23
|
## [0.7.1] - 2026-09-23
|
||||||
|
|
||||||
|
|||||||
+13
-4
@@ -46,6 +46,7 @@ docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
|
|||||||
| `S3_ENDPOINT` / `S3_REGION` / `S3_BUCKET` | S3 API endpoint (empty endpoint → AWS `s3.<region>.amazonaws.com`), region, bucket | with `s3` |
|
| `S3_ENDPOINT` / `S3_REGION` / `S3_BUCKET` | S3 API endpoint (empty endpoint → AWS `s3.<region>.amazonaws.com`), region, bucket | with `s3` |
|
||||||
| `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY` | S3 credentials | with `s3` |
|
| `S3_ACCESS_KEY_ID` / `S3_SECRET_ACCESS_KEY` | S3 credentials | with `s3` |
|
||||||
| `S3_PREFIX` / `S3_PATH_STYLE` | optional key prefix; path-style URLs (default `true`, needed by MinIO/Garage — set `false` for AWS virtual-hosted) | no |
|
| `S3_PREFIX` / `S3_PATH_STYLE` | optional key prefix; path-style URLs (default `true`, needed by MinIO/Garage — set `false` for AWS virtual-hosted) | no |
|
||||||
|
| `FUNCTIONS_RUNTIME` / `FUNCTIONS_TIMEOUT_SECONDS` / `FUNCTIONS_MAX_OUTPUT_BYTES` | Functions: runtime binary (`auto` resolves `bun`/`deno` from the backend's PATH — the backend image must ship one, or set this to its path), per-run timeout (default 30 s), captured-output cap (default 64 KiB) | no |
|
||||||
| `AUTH_ADMIN_EMAILS` | comma-separated emails promoted to auth admin on boot | no |
|
| `AUTH_ADMIN_EMAILS` | comma-separated emails promoted to auth admin on boot | no |
|
||||||
| `USER_RATE_LIMIT_PER_MINUTE` / `API_KEY_RATE_LIMIT_PER_MINUTE` | per-identity API rate limits (defaults 240 / 600) | no |
|
| `USER_RATE_LIMIT_PER_MINUTE` / `API_KEY_RATE_LIMIT_PER_MINUTE` | per-identity API rate limits (defaults 240 / 600) | no |
|
||||||
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASSWORD` / `SMTP_SECURE` / `MAIL_FROM` | transactional mail; without it verification/reset emails go nowhere | production |
|
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASSWORD` / `SMTP_SECURE` / `MAIL_FROM` | transactional mail; without it verification/reset emails go nowhere | production |
|
||||||
@@ -131,10 +132,18 @@ docker compose up -d # 3. rolling recreate; backend migrate
|
|||||||
|
|
||||||
## Storage constraint
|
## Storage constraint
|
||||||
|
|
||||||
Object storage is the **local filesystem** (`backend_storage` volume). There
|
Object storage defaults to the **local filesystem** (`backend_storage`
|
||||||
is no S3-compatible backend yet — the volume must be on durable, backed-up
|
volume) — durable, backed-up disk on a single host. For multi-node or
|
||||||
disk, and a single host serves all objects. Plan capacity accordingly;
|
external object stores set `BACKEND_STORAGE_DRIVER=s3` + the `S3_*`
|
||||||
multi-node deployments need an external object store first.
|
variables above (AWS, MinIO, Garage, R2 all work).
|
||||||
|
|
||||||
|
## Functions trust model
|
||||||
|
|
||||||
|
Function code runs as a child process of the backend — same host, same
|
||||||
|
privileges, no sandbox. Treat every project member who can edit a function
|
||||||
|
as holding a shell on the backend host. If that is unacceptable for your
|
||||||
|
deployment, do not enable untrusted editors; isolated runtimes are on the
|
||||||
|
roadmap.
|
||||||
|
|
||||||
## Observability
|
## Observability
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ Supabase, with no hosted tier - your data stays on your hardware.
|
|||||||
- **Auth** - email/password plus optional GitHub, Google, Discord, and Microsoft OAuth. JWTs verified by the API against JWKS.
|
- **Auth** - email/password plus optional GitHub, Google, Discord, and Microsoft OAuth. JWTs verified by the API against JWKS.
|
||||||
- **Storage** - S3-style buckets on local disk or any S3-compatible backend (AWS, MinIO, Garage, R2), public or private.
|
- **Storage** - S3-style buckets on local disk or any S3-compatible backend (AWS, MinIO, Garage, R2), public or private.
|
||||||
- **Collections** - schema-flexible JSON documents in Postgres JSONB, with filter/order queries.
|
- **Collections** - schema-flexible JSON documents in Postgres JSONB, with filter/order queries.
|
||||||
- **Functions** - project-scoped JS/TS executed by bun/deno on the host, with per-run logs.
|
- **Functions** - project-scoped JS/TS executed by bun/deno on the host, with per-run logs. Trigger them four ways: manual invoke, cron schedules (`function_id` on a job), inbound hooks (`mode: "function"`), or domain events (`event_pattern` like `document.*`).
|
||||||
- **Vault** - AES-256-GCM project secrets plus an Argon2id/XChaCha20 local vault in the CLI and desktop app.
|
- **Vault** - AES-256-GCM project secrets plus an Argon2id/XChaCha20 local vault in the CLI and desktop app.
|
||||||
- **Automation** - scheduled jobs, inbound webhooks, telemetry alert rules, and `secret://` refs resolved at delivery.
|
- **Automation** - scheduled jobs, inbound webhooks, telemetry alert rules, and `secret://` refs resolved at delivery.
|
||||||
- **Realtime** - SSE event stream with a typed client SDK (`@primora/client`).
|
- **Realtime** - SSE event stream with a typed client SDK (`@primora/client`).
|
||||||
|
|||||||
+7
-4
@@ -357,11 +357,14 @@ Reusable templates + an operational send log.
|
|||||||
|
|
||||||
Ordered loosely by leverage. None committed; each gets scoped when picked.
|
Ordered loosely by leverage. None committed; each gets scoped when picked.
|
||||||
|
|
||||||
- **Phase 9 — Functions** — ✅ first slice shipped: `core.functions` +
|
- **Phase 9 — Functions** — ✅ shipped: `core.functions` +
|
||||||
`core.function_runs`, bun/deno exec runner (stdin payload, env injection,
|
`core.function_runs`, bun/deno exec runner (stdin payload, env injection,
|
||||||
capped output, timeout), invoke API + dashboard page. Remaining
|
capped output, timeout), invoke API + dashboard page. Triggers shipped:
|
||||||
candidates: schedule/event/HTTP triggers riding jobs + inbound hooks,
|
scheduled jobs (`function_id` on a job), inbound hooks (`mode:
|
||||||
and isolated runtimes (sidecar/Firecracker) for untrusted code.
|
"function"`), and domain events (`event_pattern`, e.g. `document.*`).
|
||||||
|
Runs record trigger source (`manual`/`schedule`/`hook`/`event`).
|
||||||
|
Remaining candidate: isolated runtimes (sidecar/Firecracker) for
|
||||||
|
untrusted code — functions still execute with backend host privileges.
|
||||||
- **Phase 10 — Storage backends** — ✅ S3-compatible object storage shipped:
|
- **Phase 10 — Storage backends** — ✅ S3-compatible object storage shipped:
|
||||||
`BACKEND_STORAGE_DRIVER=s3` + `S3_*` env vars, stdlib SigV4, verified
|
`BACKEND_STORAGE_DRIVER=s3` + `S3_*` env vars, stdlib SigV4, verified
|
||||||
against a real S3 server. Presigned-URL uploads for large files remain a
|
against a real S3 server. Presigned-URL uploads for large files remain a
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ RUN apt-get update \
|
|||||||
&& chmod +x /usr/local/bin/dbx-mcp \
|
&& chmod +x /usr/local/bin/dbx-mcp \
|
||||||
&& rm /tmp/dbx-mcp.tar.gz
|
&& rm /tmp/dbx-mcp.tar.gz
|
||||||
|
|
||||||
|
# Functions runtime — bun static binary from the official image (multi-arch).
|
||||||
|
COPY --from=oven/bun:1.3.14 /usr/local/bin/bun /usr/local/bin/bun
|
||||||
|
|
||||||
COPY --from=builder /primora-backend /usr/local/bin/primora-backend
|
COPY --from=builder /primora-backend /usr/local/bin/primora-backend
|
||||||
COPY --from=builder /app/db ./db
|
COPY --from=builder /app/db ./db
|
||||||
COPY --from=builder /app/openapi ./openapi
|
COPY --from=builder /app/openapi ./openapi
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
-- +goose Up
|
||||||
|
ALTER TABLE core.scheduled_jobs
|
||||||
|
ADD COLUMN function_id UUID REFERENCES core.functions(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
ALTER TABLE core.inbound_hooks
|
||||||
|
ADD COLUMN function_id UUID REFERENCES core.functions(id) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE core.inbound_hooks
|
||||||
|
DROP CONSTRAINT inbound_hooks_mode_check;
|
||||||
|
ALTER TABLE core.inbound_hooks
|
||||||
|
ADD CONSTRAINT inbound_hooks_mode_check
|
||||||
|
CHECK (mode IN ('event','job','function'));
|
||||||
|
|
||||||
|
ALTER TABLE core.functions
|
||||||
|
ADD COLUMN event_pattern TEXT NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
ALTER TABLE core.function_runs
|
||||||
|
DROP CONSTRAINT function_runs_trigger_check;
|
||||||
|
ALTER TABLE core.function_runs
|
||||||
|
ADD CONSTRAINT function_runs_trigger_check
|
||||||
|
CHECK (trigger IN ('manual','schedule','hook','event'));
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE core.function_runs
|
||||||
|
DROP CONSTRAINT function_runs_trigger_check;
|
||||||
|
ALTER TABLE core.function_runs
|
||||||
|
ADD CONSTRAINT function_runs_trigger_check
|
||||||
|
CHECK (trigger IN ('manual','schedule','hook'));
|
||||||
|
ALTER TABLE core.functions DROP COLUMN IF EXISTS event_pattern;
|
||||||
|
ALTER TABLE core.inbound_hooks
|
||||||
|
DROP CONSTRAINT inbound_hooks_mode_check;
|
||||||
|
ALTER TABLE core.inbound_hooks
|
||||||
|
ADD CONSTRAINT inbound_hooks_mode_check CHECK (mode IN ('event','job'));
|
||||||
|
ALTER TABLE core.inbound_hooks DROP COLUMN IF EXISTS function_id;
|
||||||
|
ALTER TABLE core.scheduled_jobs DROP COLUMN IF EXISTS function_id;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
-- name: CreateFunction :one
|
-- name: CreateFunction :one
|
||||||
INSERT INTO core.functions (project_id, name, code, runtime)
|
INSERT INTO core.functions (project_id, name, code, runtime, event_pattern)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: GetFunction :one
|
-- name: GetFunction :one
|
||||||
@@ -18,6 +18,7 @@ ORDER BY name;
|
|||||||
UPDATE core.functions
|
UPDATE core.functions
|
||||||
SET code = COALESCE(sqlc.narg('code'), code),
|
SET code = COALESCE(sqlc.narg('code'), code),
|
||||||
enabled = COALESCE(sqlc.narg('enabled'), enabled),
|
enabled = COALESCE(sqlc.narg('enabled'), enabled),
|
||||||
|
event_pattern = COALESCE(sqlc.narg('event_pattern'), event_pattern),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1 AND project_id = $2
|
WHERE id = $1 AND project_id = $2
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
@@ -35,3 +36,10 @@ SELECT * FROM core.function_runs
|
|||||||
WHERE function_id = $1
|
WHERE function_id = $1
|
||||||
ORDER BY created_at DESC
|
ORDER BY created_at DESC
|
||||||
LIMIT $2;
|
LIMIT $2;
|
||||||
|
|
||||||
|
-- name: ListFunctionsForEvent :many
|
||||||
|
SELECT * FROM core.functions
|
||||||
|
WHERE project_id = $1
|
||||||
|
AND enabled = TRUE
|
||||||
|
AND event_pattern <> ''
|
||||||
|
AND ($2 LIKE REPLACE(event_pattern, '*', '%') OR event_pattern = '*');
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
-- name: CreateInboundHook :one
|
-- name: CreateInboundHook :one
|
||||||
INSERT INTO core.inbound_hooks (project_id, name, token, mode, job_id, secret, enabled)
|
INSERT INTO core.inbound_hooks (project_id, name, token, mode, job_id, function_id, secret, enabled)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: ListInboundHooks :many
|
-- name: ListInboundHooks :many
|
||||||
|
|||||||
@@ -17,8 +17,9 @@ INSERT INTO core.scheduled_jobs (
|
|||||||
payload,
|
payload,
|
||||||
enabled,
|
enabled,
|
||||||
next_run_at,
|
next_run_at,
|
||||||
created_by_user_id
|
created_by_user_id,
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
function_id
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
-- name: UpdateScheduledJob :one
|
-- name: UpdateScheduledJob :one
|
||||||
@@ -30,6 +31,7 @@ SET name = $3,
|
|||||||
next_run_at = $7,
|
next_run_at = $7,
|
||||||
secret = CASE WHEN $8::boolean THEN $9 ELSE secret END,
|
secret = CASE WHEN $8::boolean THEN $9 ELSE secret END,
|
||||||
payload = CASE WHEN $10::boolean THEN $11 ELSE payload END,
|
payload = CASE WHEN $10::boolean THEN $11 ELSE payload END,
|
||||||
|
function_id = CASE WHEN $12::boolean THEN $13 ELSE function_id END,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND project_id = $2
|
AND project_id = $2
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const createFunction = `-- name: CreateFunction :one
|
const createFunction = `-- name: CreateFunction :one
|
||||||
INSERT INTO core.functions (project_id, name, code, runtime)
|
INSERT INTO core.functions (project_id, name, code, runtime, event_pattern)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING id, project_id, name, code, runtime, enabled, created_at, updated_at
|
RETURNING id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateFunctionParams struct {
|
type CreateFunctionParams struct {
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Runtime string `json:"runtime"`
|
Runtime string `json:"runtime"`
|
||||||
|
EventPattern string `json:"event_pattern"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateFunction(ctx context.Context, arg CreateFunctionParams) (CoreFunction, error) {
|
func (q *Queries) CreateFunction(ctx context.Context, arg CreateFunctionParams) (CoreFunction, error) {
|
||||||
@@ -30,6 +31,7 @@ func (q *Queries) CreateFunction(ctx context.Context, arg CreateFunctionParams)
|
|||||||
arg.Name,
|
arg.Name,
|
||||||
arg.Code,
|
arg.Code,
|
||||||
arg.Runtime,
|
arg.Runtime,
|
||||||
|
arg.EventPattern,
|
||||||
)
|
)
|
||||||
var i CoreFunction
|
var i CoreFunction
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -41,6 +43,7 @@ func (q *Queries) CreateFunction(ctx context.Context, arg CreateFunctionParams)
|
|||||||
&i.Enabled,
|
&i.Enabled,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -60,7 +63,7 @@ func (q *Queries) DeleteFunction(ctx context.Context, arg DeleteFunctionParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getFunction = `-- name: GetFunction :one
|
const getFunction = `-- name: GetFunction :one
|
||||||
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at FROM core.functions WHERE id = $1
|
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern FROM core.functions WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetFunction(ctx context.Context, id uuid.UUID) (CoreFunction, error) {
|
func (q *Queries) GetFunction(ctx context.Context, id uuid.UUID) (CoreFunction, error) {
|
||||||
@@ -75,12 +78,13 @@ func (q *Queries) GetFunction(ctx context.Context, id uuid.UUID) (CoreFunction,
|
|||||||
&i.Enabled,
|
&i.Enabled,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFunctionByName = `-- name: GetFunctionByName :one
|
const getFunctionByName = `-- name: GetFunctionByName :one
|
||||||
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at FROM core.functions WHERE project_id = $1 AND name = $2
|
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern FROM core.functions WHERE project_id = $1 AND name = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetFunctionByNameParams struct {
|
type GetFunctionByNameParams struct {
|
||||||
@@ -100,6 +104,7 @@ func (q *Queries) GetFunctionByName(ctx context.Context, arg GetFunctionByNamePa
|
|||||||
&i.Enabled,
|
&i.Enabled,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -188,7 +193,7 @@ func (q *Queries) ListFunctionRuns(ctx context.Context, arg ListFunctionRunsPara
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listFunctions = `-- name: ListFunctions :many
|
const listFunctions = `-- name: ListFunctions :many
|
||||||
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at FROM core.functions
|
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern FROM core.functions
|
||||||
WHERE project_id = $1
|
WHERE project_id = $1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
`
|
`
|
||||||
@@ -211,6 +216,50 @@ func (q *Queries) ListFunctions(ctx context.Context, projectID uuid.UUID) ([]Cor
|
|||||||
&i.Enabled,
|
&i.Enabled,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const listFunctionsForEvent = `-- name: ListFunctionsForEvent :many
|
||||||
|
SELECT id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern FROM core.functions
|
||||||
|
WHERE project_id = $1
|
||||||
|
AND enabled = TRUE
|
||||||
|
AND event_pattern <> ''
|
||||||
|
AND ($2 LIKE REPLACE(event_pattern, '*', '%') OR event_pattern = '*')
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListFunctionsForEventParams struct {
|
||||||
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
|
EventPattern string `json:"event_pattern"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListFunctionsForEvent(ctx context.Context, arg ListFunctionsForEventParams) ([]CoreFunction, error) {
|
||||||
|
rows, err := q.db.Query(ctx, listFunctionsForEvent, arg.ProjectID, arg.EventPattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []CoreFunction
|
||||||
|
for rows.Next() {
|
||||||
|
var i CoreFunction
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.ProjectID,
|
||||||
|
&i.Name,
|
||||||
|
&i.Code,
|
||||||
|
&i.Runtime,
|
||||||
|
&i.Enabled,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -226,16 +275,18 @@ const updateFunction = `-- name: UpdateFunction :one
|
|||||||
UPDATE core.functions
|
UPDATE core.functions
|
||||||
SET code = COALESCE($3, code),
|
SET code = COALESCE($3, code),
|
||||||
enabled = COALESCE($4, enabled),
|
enabled = COALESCE($4, enabled),
|
||||||
|
event_pattern = COALESCE($5, event_pattern),
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1 AND project_id = $2
|
WHERE id = $1 AND project_id = $2
|
||||||
RETURNING id, project_id, name, code, runtime, enabled, created_at, updated_at
|
RETURNING id, project_id, name, code, runtime, enabled, created_at, updated_at, event_pattern
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateFunctionParams struct {
|
type UpdateFunctionParams struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Code *string `json:"code"`
|
Code *string `json:"code"`
|
||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
|
EventPattern *string `json:"event_pattern"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateFunction(ctx context.Context, arg UpdateFunctionParams) (CoreFunction, error) {
|
func (q *Queries) UpdateFunction(ctx context.Context, arg UpdateFunctionParams) (CoreFunction, error) {
|
||||||
@@ -244,6 +295,7 @@ func (q *Queries) UpdateFunction(ctx context.Context, arg UpdateFunctionParams)
|
|||||||
arg.ProjectID,
|
arg.ProjectID,
|
||||||
arg.Code,
|
arg.Code,
|
||||||
arg.Enabled,
|
arg.Enabled,
|
||||||
|
arg.EventPattern,
|
||||||
)
|
)
|
||||||
var i CoreFunction
|
var i CoreFunction
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -255,6 +307,7 @@ func (q *Queries) UpdateFunction(ctx context.Context, arg UpdateFunctionParams)
|
|||||||
&i.Enabled,
|
&i.Enabled,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.EventPattern,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -13,19 +13,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const createInboundHook = `-- name: CreateInboundHook :one
|
const createInboundHook = `-- name: CreateInboundHook :one
|
||||||
INSERT INTO core.inbound_hooks (project_id, name, token, mode, job_id, secret, enabled)
|
INSERT INTO core.inbound_hooks (project_id, name, token, mode, job_id, function_id, secret, enabled)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||||
RETURNING id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at
|
RETURNING id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at, function_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateInboundHookParams struct {
|
type CreateInboundHookParams struct {
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
JobID pgtype.UUID `json:"job_id"`
|
JobID pgtype.UUID `json:"job_id"`
|
||||||
Secret []byte `json:"secret"`
|
FunctionID pgtype.UUID `json:"function_id"`
|
||||||
Enabled bool `json:"enabled"`
|
Secret []byte `json:"secret"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateInboundHook(ctx context.Context, arg CreateInboundHookParams) (CoreInboundHook, error) {
|
func (q *Queries) CreateInboundHook(ctx context.Context, arg CreateInboundHookParams) (CoreInboundHook, error) {
|
||||||
@@ -35,6 +36,7 @@ func (q *Queries) CreateInboundHook(ctx context.Context, arg CreateInboundHookPa
|
|||||||
arg.Token,
|
arg.Token,
|
||||||
arg.Mode,
|
arg.Mode,
|
||||||
arg.JobID,
|
arg.JobID,
|
||||||
|
arg.FunctionID,
|
||||||
arg.Secret,
|
arg.Secret,
|
||||||
arg.Enabled,
|
arg.Enabled,
|
||||||
)
|
)
|
||||||
@@ -51,6 +53,7 @@ func (q *Queries) CreateInboundHook(ctx context.Context, arg CreateInboundHookPa
|
|||||||
&i.LastReceivedAt,
|
&i.LastReceivedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -71,7 +74,7 @@ func (q *Queries) DeleteInboundHook(ctx context.Context, arg DeleteInboundHookPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getInboundHook = `-- name: GetInboundHook :one
|
const getInboundHook = `-- name: GetInboundHook :one
|
||||||
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at FROM core.inbound_hooks
|
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at, function_id FROM core.inbound_hooks
|
||||||
WHERE id = $1 AND project_id = $2
|
WHERE id = $1 AND project_id = $2
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -95,12 +98,13 @@ func (q *Queries) GetInboundHook(ctx context.Context, arg GetInboundHookParams)
|
|||||||
&i.LastReceivedAt,
|
&i.LastReceivedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getInboundHookByToken = `-- name: GetInboundHookByToken :one
|
const getInboundHookByToken = `-- name: GetInboundHookByToken :one
|
||||||
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at FROM core.inbound_hooks
|
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at, function_id FROM core.inbound_hooks
|
||||||
WHERE token = $1
|
WHERE token = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -119,12 +123,13 @@ func (q *Queries) GetInboundHookByToken(ctx context.Context, token string) (Core
|
|||||||
&i.LastReceivedAt,
|
&i.LastReceivedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const listInboundHooks = `-- name: ListInboundHooks :many
|
const listInboundHooks = `-- name: ListInboundHooks :many
|
||||||
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at FROM core.inbound_hooks
|
SELECT id, project_id, name, token, mode, job_id, secret, enabled, last_received_at, created_at, updated_at, function_id FROM core.inbound_hooks
|
||||||
WHERE project_id = $1
|
WHERE project_id = $1
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
`
|
`
|
||||||
@@ -150,6 +155,7 @@ func (q *Queries) ListInboundHooks(ctx context.Context, projectID uuid.UUID) ([]
|
|||||||
&i.LastReceivedAt,
|
&i.LastReceivedAt,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ INSERT INTO core.scheduled_jobs (
|
|||||||
payload,
|
payload,
|
||||||
enabled,
|
enabled,
|
||||||
next_run_at,
|
next_run_at,
|
||||||
created_by_user_id
|
created_by_user_id,
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
function_id
|
||||||
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||||
|
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateScheduledJobParams struct {
|
type CreateScheduledJobParams struct {
|
||||||
@@ -37,6 +38,7 @@ type CreateScheduledJobParams struct {
|
|||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||||
CreatedByUserID pgtype.UUID `json:"created_by_user_id"`
|
CreatedByUserID pgtype.UUID `json:"created_by_user_id"`
|
||||||
|
FunctionID pgtype.UUID `json:"function_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateScheduledJob(ctx context.Context, arg CreateScheduledJobParams) (CoreScheduledJob, error) {
|
func (q *Queries) CreateScheduledJob(ctx context.Context, arg CreateScheduledJobParams) (CoreScheduledJob, error) {
|
||||||
@@ -50,6 +52,7 @@ func (q *Queries) CreateScheduledJob(ctx context.Context, arg CreateScheduledJob
|
|||||||
arg.Enabled,
|
arg.Enabled,
|
||||||
arg.NextRunAt,
|
arg.NextRunAt,
|
||||||
arg.CreatedByUserID,
|
arg.CreatedByUserID,
|
||||||
|
arg.FunctionID,
|
||||||
)
|
)
|
||||||
var i CoreScheduledJob
|
var i CoreScheduledJob
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -67,6 +70,7 @@ func (q *Queries) CreateScheduledJob(ctx context.Context, arg CreateScheduledJob
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,7 @@ func (q *Queries) FinishScheduledJobRun(ctx context.Context, arg FinishScheduled
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getScheduledJobByID = `-- name: GetScheduledJobByID :one
|
const getScheduledJobByID = `-- name: GetScheduledJobByID :one
|
||||||
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at FROM core.scheduled_jobs
|
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id FROM core.scheduled_jobs
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -155,6 +159,7 @@ func (q *Queries) GetScheduledJobByID(ctx context.Context, id uuid.UUID) (CoreSc
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -188,7 +193,7 @@ func (q *Queries) InsertScheduledJobRun(ctx context.Context, arg InsertScheduled
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listDueScheduledJobs = `-- name: ListDueScheduledJobs :many
|
const listDueScheduledJobs = `-- name: ListDueScheduledJobs :many
|
||||||
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at FROM core.scheduled_jobs
|
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id FROM core.scheduled_jobs
|
||||||
WHERE enabled
|
WHERE enabled
|
||||||
AND next_run_at IS NOT NULL
|
AND next_run_at IS NOT NULL
|
||||||
AND next_run_at <= $1
|
AND next_run_at <= $1
|
||||||
@@ -218,6 +223,7 @@ func (q *Queries) ListDueScheduledJobs(ctx context.Context, nextRunAt pgtype.Tim
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -272,7 +278,7 @@ func (q *Queries) ListScheduledJobRuns(ctx context.Context, arg ListScheduledJob
|
|||||||
}
|
}
|
||||||
|
|
||||||
const listScheduledJobs = `-- name: ListScheduledJobs :many
|
const listScheduledJobs = `-- name: ListScheduledJobs :many
|
||||||
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at FROM core.scheduled_jobs
|
SELECT id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id FROM core.scheduled_jobs
|
||||||
WHERE project_id = $1
|
WHERE project_id = $1
|
||||||
ORDER BY created_at ASC
|
ORDER BY created_at ASC
|
||||||
`
|
`
|
||||||
@@ -301,6 +307,7 @@ func (q *Queries) ListScheduledJobs(ctx context.Context, projectID uuid.UUID) ([
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -319,7 +326,7 @@ SET last_run_at = $2,
|
|||||||
next_run_at = $4,
|
next_run_at = $4,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at
|
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type MarkScheduledJobRanParams struct {
|
type MarkScheduledJobRanParams struct {
|
||||||
@@ -352,6 +359,7 @@ func (q *Queries) MarkScheduledJobRan(ctx context.Context, arg MarkScheduledJobR
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -386,24 +394,27 @@ SET name = $3,
|
|||||||
next_run_at = $7,
|
next_run_at = $7,
|
||||||
secret = CASE WHEN $8::boolean THEN $9 ELSE secret END,
|
secret = CASE WHEN $8::boolean THEN $9 ELSE secret END,
|
||||||
payload = CASE WHEN $10::boolean THEN $11 ELSE payload END,
|
payload = CASE WHEN $10::boolean THEN $11 ELSE payload END,
|
||||||
|
function_id = CASE WHEN $12::boolean THEN $13 ELSE function_id END,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
AND project_id = $2
|
AND project_id = $2
|
||||||
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at
|
RETURNING id, project_id, name, schedule, url, secret, payload, enabled, last_run_at, last_status, next_run_at, created_by_user_id, created_at, updated_at, function_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateScheduledJobParams struct {
|
type UpdateScheduledJobParams struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Schedule string `json:"schedule"`
|
Schedule string `json:"schedule"`
|
||||||
Url string `json:"url"`
|
Url string `json:"url"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||||
Column8 bool `json:"column_8"`
|
Column8 bool `json:"column_8"`
|
||||||
Secret []byte `json:"secret"`
|
Secret []byte `json:"secret"`
|
||||||
Column10 bool `json:"column_10"`
|
Column10 bool `json:"column_10"`
|
||||||
Payload []byte `json:"payload"`
|
Payload []byte `json:"payload"`
|
||||||
|
Column12 bool `json:"column_12"`
|
||||||
|
FunctionID pgtype.UUID `json:"function_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJobParams) (CoreScheduledJob, error) {
|
func (q *Queries) UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJobParams) (CoreScheduledJob, error) {
|
||||||
@@ -419,6 +430,8 @@ func (q *Queries) UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJob
|
|||||||
arg.Secret,
|
arg.Secret,
|
||||||
arg.Column10,
|
arg.Column10,
|
||||||
arg.Payload,
|
arg.Payload,
|
||||||
|
arg.Column12,
|
||||||
|
arg.FunctionID,
|
||||||
)
|
)
|
||||||
var i CoreScheduledJob
|
var i CoreScheduledJob
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -436,6 +449,7 @@ func (q *Queries) UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJob
|
|||||||
&i.CreatedByUserID,
|
&i.CreatedByUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
|
&i.FunctionID,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -267,14 +267,15 @@ type CoreEvent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CoreFunction struct {
|
type CoreFunction struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ProjectID uuid.UUID `json:"project_id"`
|
ProjectID uuid.UUID `json:"project_id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
Runtime string `json:"runtime"`
|
Runtime string `json:"runtime"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
EventPattern string `json:"event_pattern"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoreFunctionRun struct {
|
type CoreFunctionRun struct {
|
||||||
@@ -301,6 +302,7 @@ type CoreInboundHook struct {
|
|||||||
LastReceivedAt pgtype.Timestamptz `json:"last_received_at"`
|
LastReceivedAt pgtype.Timestamptz `json:"last_received_at"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
FunctionID pgtype.UUID `json:"function_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoreIntegration struct {
|
type CoreIntegration struct {
|
||||||
@@ -393,6 +395,7 @@ type CoreScheduledJob struct {
|
|||||||
CreatedByUserID pgtype.UUID `json:"created_by_user_id"`
|
CreatedByUserID pgtype.UUID `json:"created_by_user_id"`
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
|
FunctionID pgtype.UUID `json:"function_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CoreScheduledJobRun struct {
|
type CoreScheduledJobRun struct {
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ type Querier interface {
|
|||||||
ListEvents(ctx context.Context, arg ListEventsParams) ([]ListEventsRow, error)
|
ListEvents(ctx context.Context, arg ListEventsParams) ([]ListEventsRow, error)
|
||||||
ListFunctionRuns(ctx context.Context, arg ListFunctionRunsParams) ([]CoreFunctionRun, error)
|
ListFunctionRuns(ctx context.Context, arg ListFunctionRunsParams) ([]CoreFunctionRun, error)
|
||||||
ListFunctions(ctx context.Context, projectID uuid.UUID) ([]CoreFunction, error)
|
ListFunctions(ctx context.Context, projectID uuid.UUID) ([]CoreFunction, error)
|
||||||
|
ListFunctionsForEvent(ctx context.Context, arg ListFunctionsForEventParams) ([]CoreFunction, error)
|
||||||
ListInboundHooks(ctx context.Context, projectID uuid.UUID) ([]CoreInboundHook, error)
|
ListInboundHooks(ctx context.Context, projectID uuid.UUID) ([]CoreInboundHook, error)
|
||||||
ListIntegrations(ctx context.Context, projectID uuid.UUID) ([]CoreIntegration, error)
|
ListIntegrations(ctx context.Context, projectID uuid.UUID) ([]CoreIntegration, error)
|
||||||
ListInvitationsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListInvitationsForOrganizationRow, error)
|
ListInvitationsForOrganization(ctx context.Context, organizationID uuid.UUID) ([]ListInvitationsForOrganizationRow, error)
|
||||||
|
|||||||
@@ -148,23 +148,26 @@ func (l *limitedWriter) Write(p []byte) (int, error) {
|
|||||||
// --- service layer -------------------------------------------------------
|
// --- service layer -------------------------------------------------------
|
||||||
|
|
||||||
type CreateFunctionInput struct {
|
type CreateFunctionInput struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Code string `json:"code" validate:"required,max=262144"`
|
Code string `json:"code" validate:"required,max=262144"`
|
||||||
Runtime string `json:"runtime" validate:"omitempty,oneof=bun deno"`
|
Runtime string `json:"runtime" validate:"omitempty,oneof=bun deno"`
|
||||||
|
EventPattern string `json:"event_pattern" validate:"omitempty,max=128"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFunctionInput struct {
|
type UpdateFunctionInput struct {
|
||||||
Code *string `json:"code" validate:"omitempty,max=262144"`
|
Code *string `json:"code" validate:"omitempty,max=262144"`
|
||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
|
EventPattern *string `json:"event_pattern" validate:"omitempty,max=128"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionSummary struct {
|
type FunctionSummary struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Runtime string `json:"runtime"`
|
Runtime string `json:"runtime"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
EventPattern string `json:"event_pattern,omitempty"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionRunSummary struct {
|
type FunctionRunSummary struct {
|
||||||
@@ -180,12 +183,13 @@ type FunctionRunSummary struct {
|
|||||||
|
|
||||||
func functionSummary(r db.CoreFunction) FunctionSummary {
|
func functionSummary(r db.CoreFunction) FunctionSummary {
|
||||||
return FunctionSummary{
|
return FunctionSummary{
|
||||||
ID: r.ID.String(),
|
ID: r.ID.String(),
|
||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
Runtime: r.Runtime,
|
Runtime: r.Runtime,
|
||||||
Enabled: r.Enabled,
|
Enabled: r.Enabled,
|
||||||
CreatedAt: r.CreatedAt.Time,
|
EventPattern: r.EventPattern,
|
||||||
UpdatedAt: r.UpdatedAt.Time,
|
CreatedAt: r.CreatedAt.Time,
|
||||||
|
UpdatedAt: r.UpdatedAt.Time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,10 +219,11 @@ func (s *PlatformService) CreateFunction(ctx context.Context, actor *models.Acto
|
|||||||
runtime = "bun"
|
runtime = "bun"
|
||||||
}
|
}
|
||||||
row, err := s.repo.Queries().CreateFunction(ctx, db.CreateFunctionParams{
|
row, err := s.repo.Queries().CreateFunction(ctx, db.CreateFunctionParams{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Code: input.Code,
|
Code: input.Code,
|
||||||
Runtime: runtime,
|
Runtime: runtime,
|
||||||
|
EventPattern: strings.TrimSpace(input.EventPattern),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FunctionSummary{}, err
|
return FunctionSummary{}, err
|
||||||
@@ -262,11 +267,17 @@ func (s *PlatformService) UpdateFunction(ctx context.Context, actor *models.Acto
|
|||||||
if err := s.requireProjectRole(ctx, actor, row.ProjectID, "admin", "developer"); err != nil {
|
if err := s.requireProjectRole(ctx, actor, row.ProjectID, "admin", "developer"); err != nil {
|
||||||
return FunctionSummary{}, err
|
return FunctionSummary{}, err
|
||||||
}
|
}
|
||||||
|
var pattern *string
|
||||||
|
if input.EventPattern != nil {
|
||||||
|
p := strings.TrimSpace(*input.EventPattern)
|
||||||
|
pattern = &p
|
||||||
|
}
|
||||||
updated, err := s.repo.Queries().UpdateFunction(ctx, db.UpdateFunctionParams{
|
updated, err := s.repo.Queries().UpdateFunction(ctx, db.UpdateFunctionParams{
|
||||||
ID: functionID,
|
ID: functionID,
|
||||||
ProjectID: row.ProjectID,
|
ProjectID: row.ProjectID,
|
||||||
Code: input.Code,
|
Code: input.Code,
|
||||||
Enabled: input.Enabled,
|
Enabled: input.Enabled,
|
||||||
|
EventPattern: pattern,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return FunctionSummary{}, err
|
return FunctionSummary{}, err
|
||||||
@@ -298,6 +309,13 @@ func (s *PlatformService) InvokeFunction(ctx context.Context, actor *models.Acto
|
|||||||
if err := s.requireProjectRole(ctx, actor, row.ProjectID, "admin", "developer"); err != nil {
|
if err := s.requireProjectRole(ctx, actor, row.ProjectID, "admin", "developer"); err != nil {
|
||||||
return FunctionRunSummary{}, err
|
return FunctionRunSummary{}, err
|
||||||
}
|
}
|
||||||
|
return s.executeFunction(ctx, row, payload, "manual")
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeFunction runs a function row and records the outcome — shared by
|
||||||
|
// manual invoke, schedule/hook delivery, and event triggers (no role check;
|
||||||
|
// callers hold their own auth context or are system paths).
|
||||||
|
func (s *PlatformService) executeFunction(ctx context.Context, row db.CoreFunction, payload json.RawMessage, trigger string) (FunctionRunSummary, error) {
|
||||||
if !row.Enabled {
|
if !row.Enabled {
|
||||||
return FunctionRunSummary{}, fmt.Errorf("function is disabled")
|
return FunctionRunSummary{}, fmt.Errorf("function is disabled")
|
||||||
}
|
}
|
||||||
@@ -317,7 +335,7 @@ func (s *PlatformService) InvokeFunction(ctx context.Context, actor *models.Acto
|
|||||||
}
|
}
|
||||||
run, err := s.repo.Queries().InsertFunctionRun(ctx, db.InsertFunctionRunParams{
|
run, err := s.repo.Queries().InsertFunctionRun(ctx, db.InsertFunctionRunParams{
|
||||||
FunctionID: row.ID,
|
FunctionID: row.ID,
|
||||||
Trigger: "manual",
|
Trigger: trigger,
|
||||||
Status: result.Status,
|
Status: result.Status,
|
||||||
ExitCode: exit,
|
ExitCode: exit,
|
||||||
Stdout: result.Stdout,
|
Stdout: result.Stdout,
|
||||||
@@ -330,6 +348,79 @@ func (s *PlatformService) InvokeFunction(ctx context.Context, actor *models.Acto
|
|||||||
return runSummary(run), nil
|
return runSummary(run), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runJobFunction is the scheduler's delivery path for function-targeted
|
||||||
|
// jobs: load the function, run it with the resolved payload, record the
|
||||||
|
// run. The job run's status_code mirrors the process exit code.
|
||||||
|
func (s *PlatformService) runJobFunction(ctx context.Context, job db.CoreScheduledJob, run db.CoreScheduledJobRun, payload json.RawMessage) (*int32, error) {
|
||||||
|
fn, err := s.repo.Queries().GetFunction(ctx, uuid.UUID(job.FunctionID.Bytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("target function missing: %w", err)
|
||||||
|
}
|
||||||
|
summary, err := s.executeFunction(ctx, fn, payload, triggerFromJobRun(run.TriggeredBy))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
code := int32(0)
|
||||||
|
if summary.ExitCode != nil {
|
||||||
|
code = *summary.ExitCode
|
||||||
|
}
|
||||||
|
if summary.Status != "success" {
|
||||||
|
return &code, fmt.Errorf("function %q %s: %s", fn.Name, summary.Status, truncate(summary.Stderr, 200))
|
||||||
|
}
|
||||||
|
return &code, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func truncate(s string, n int) string {
|
||||||
|
if len(s) <= n {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return s[:n] + "…"
|
||||||
|
}
|
||||||
|
|
||||||
|
func triggerFromJobRun(triggeredBy string) string {
|
||||||
|
switch triggeredBy {
|
||||||
|
case "schedule", "hook", "manual":
|
||||||
|
return triggeredBy
|
||||||
|
default:
|
||||||
|
return "schedule"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerFunctionsForEvent fans a domain event out to every enabled
|
||||||
|
// function whose event_pattern matches ("document.*", "*", …). Runs are
|
||||||
|
// detached — event publishers never block on user code.
|
||||||
|
// jarvis: ceiling pattern is a single * suffix/prefix wildcard via SQL
|
||||||
|
// LIKE; upgrade to a real matcher if multi-segment globs are needed.
|
||||||
|
func (s *PlatformService) triggerFunctionsForEvent(ctx context.Context, projectID uuid.UUID, eventType string, data map[string]any) {
|
||||||
|
if s.functions == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := s.repo.Queries().ListFunctionsForEvent(ctx, db.ListFunctionsForEventParams{
|
||||||
|
ProjectID: projectID,
|
||||||
|
EventPattern: eventType,
|
||||||
|
})
|
||||||
|
if err != nil || len(rows) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"event": eventType,
|
||||||
|
"project_id": projectID.String(),
|
||||||
|
"occurred_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
for _, fn := range rows {
|
||||||
|
fn := fn
|
||||||
|
go func() {
|
||||||
|
// Detached context: the publishing request may finish first.
|
||||||
|
runCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := s.executeFunction(runCtx, fn, payload, "event"); err != nil {
|
||||||
|
s.logger.Warn("function event trigger failed", "function", fn.Name, "event", eventType, "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *PlatformService) ListFunctionRuns(ctx context.Context, actor *models.Actor, functionID uuid.UUID, limit int64) ([]FunctionRunSummary, error) {
|
func (s *PlatformService) ListFunctionRuns(ctx context.Context, actor *models.Actor, functionID uuid.UUID, limit int64) ([]FunctionRunSummary, error) {
|
||||||
row, err := s.repo.Queries().GetFunction(ctx, functionID)
|
row, err := s.repo.Queries().GetFunction(ctx, functionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -70,3 +70,17 @@ func TestLimitedWriterCaps(t *testing.T) {
|
|||||||
t.Fatalf("got %q", buf.String())
|
t.Fatalf("got %q", buf.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTriggerFromJobRun(t *testing.T) {
|
||||||
|
for in, want := range map[string]string{
|
||||||
|
"schedule": "schedule",
|
||||||
|
"hook": "hook",
|
||||||
|
"manual": "manual",
|
||||||
|
"cli": "schedule",
|
||||||
|
"": "schedule",
|
||||||
|
} {
|
||||||
|
if got := triggerFromJobRun(in); got != want {
|
||||||
|
t.Fatalf("triggerFromJobRun(%q) = %q, want %q", in, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,8 +28,9 @@ import (
|
|||||||
const (
|
const (
|
||||||
WebhookEventInboundReceived = "inbound.received"
|
WebhookEventInboundReceived = "inbound.received"
|
||||||
|
|
||||||
InboundModeEvent = "event"
|
InboundModeEvent = "event"
|
||||||
InboundModeJob = "job"
|
InboundModeJob = "job"
|
||||||
|
InboundModeFunction = "function"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InboundHookSummary struct {
|
type InboundHookSummary struct {
|
||||||
@@ -40,6 +41,7 @@ type InboundHookSummary struct {
|
|||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
JobID string `json:"job_id,omitempty"`
|
JobID string `json:"job_id,omitempty"`
|
||||||
|
FunctionID string `json:"function_id,omitempty"`
|
||||||
HasSecret bool `json:"has_secret"`
|
HasSecret bool `json:"has_secret"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
LastReceivedAt *time.Time `json:"last_received_at"`
|
LastReceivedAt *time.Time `json:"last_received_at"`
|
||||||
@@ -47,11 +49,12 @@ type InboundHookSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateInboundHookInput struct {
|
type CreateInboundHookInput struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
JobID string `json:"job_id"`
|
JobID string `json:"job_id"`
|
||||||
Secret string `json:"secret"`
|
FunctionID string `json:"function_id"`
|
||||||
Enabled *bool `json:"enabled"`
|
Secret string `json:"secret"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InboundReceiveResult struct {
|
type InboundReceiveResult struct {
|
||||||
@@ -75,6 +78,9 @@ func inboundHookSummary(row db.CoreInboundHook, publicURL string) InboundHookSum
|
|||||||
if row.JobID.Valid {
|
if row.JobID.Valid {
|
||||||
out.JobID = uuid.UUID(row.JobID.Bytes).String()
|
out.JobID = uuid.UUID(row.JobID.Bytes).String()
|
||||||
}
|
}
|
||||||
|
if row.FunctionID.Valid {
|
||||||
|
out.FunctionID = uuid.UUID(row.FunctionID.Bytes).String()
|
||||||
|
}
|
||||||
if row.LastReceivedAt.Valid {
|
if row.LastReceivedAt.Valid {
|
||||||
t := row.LastReceivedAt.Time
|
t := row.LastReceivedAt.Time
|
||||||
out.LastReceivedAt = &t
|
out.LastReceivedAt = &t
|
||||||
@@ -108,8 +114,9 @@ func (s *PlatformService) CreateInboundHook(ctx context.Context, actor *models.A
|
|||||||
if mode == "" {
|
if mode == "" {
|
||||||
mode = InboundModeEvent
|
mode = InboundModeEvent
|
||||||
}
|
}
|
||||||
var jobID pgtype.UUID
|
var jobID, functionID pgtype.UUID
|
||||||
if mode == InboundModeJob {
|
switch mode {
|
||||||
|
case InboundModeJob:
|
||||||
parsed, err := uuid.Parse(input.JobID)
|
parsed, err := uuid.Parse(input.JobID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return InboundHookSummary{}, errors.New("job mode requires a valid job_id")
|
return InboundHookSummary{}, errors.New("job mode requires a valid job_id")
|
||||||
@@ -119,8 +126,19 @@ func (s *PlatformService) CreateInboundHook(ctx context.Context, actor *models.A
|
|||||||
return InboundHookSummary{}, errors.New("job not found in this project")
|
return InboundHookSummary{}, errors.New("job not found in this project")
|
||||||
}
|
}
|
||||||
jobID = pgtype.UUID{Bytes: parsed, Valid: true}
|
jobID = pgtype.UUID{Bytes: parsed, Valid: true}
|
||||||
} else if mode != InboundModeEvent {
|
case InboundModeFunction:
|
||||||
return InboundHookSummary{}, fmt.Errorf("invalid mode %q — event or job", input.Mode)
|
parsed, err := uuid.Parse(input.FunctionID)
|
||||||
|
if err != nil {
|
||||||
|
return InboundHookSummary{}, errors.New("function mode requires a valid function_id")
|
||||||
|
}
|
||||||
|
fn, err := s.repo.Queries().GetFunction(ctx, parsed)
|
||||||
|
if err != nil || fn.ProjectID != projectID {
|
||||||
|
return InboundHookSummary{}, errors.New("function not found in this project")
|
||||||
|
}
|
||||||
|
functionID = pgtype.UUID{Bytes: parsed, Valid: true}
|
||||||
|
case InboundModeEvent:
|
||||||
|
default:
|
||||||
|
return InboundHookSummary{}, fmt.Errorf("invalid mode %q — event, job, or function", input.Mode)
|
||||||
}
|
}
|
||||||
secretBytes := []byte{}
|
secretBytes := []byte{}
|
||||||
if input.Secret != "" {
|
if input.Secret != "" {
|
||||||
@@ -141,10 +159,11 @@ func (s *PlatformService) CreateInboundHook(ctx context.Context, actor *models.A
|
|||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
Name: input.Name,
|
Name: input.Name,
|
||||||
Token: token,
|
Token: token,
|
||||||
Mode: mode,
|
Mode: mode,
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
Secret: secretBytes,
|
FunctionID: functionID,
|
||||||
Enabled: enabled,
|
Secret: secretBytes,
|
||||||
|
Enabled: enabled,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "inbound_hooks_project_id_name_key") {
|
if strings.Contains(err.Error(), "inbound_hooks_project_id_name_key") {
|
||||||
@@ -230,6 +249,31 @@ func (s *PlatformService) ReceiveInboundHook(ctx context.Context, token string,
|
|||||||
return InboundReceiveResult{}, err
|
return InboundReceiveResult{}, err
|
||||||
}
|
}
|
||||||
return InboundReceiveResult{Received: true, Event: WebhookEventJobRun, RunID: run.ID.String()}, nil
|
return InboundReceiveResult{Received: true, Event: WebhookEventJobRun, RunID: run.ID.String()}, nil
|
||||||
|
case InboundModeFunction:
|
||||||
|
if s.functions == nil {
|
||||||
|
return InboundReceiveResult{}, errors.New("functions runtime not configured")
|
||||||
|
}
|
||||||
|
if !hook.FunctionID.Valid {
|
||||||
|
return InboundReceiveResult{}, errors.New("hook has no function configured")
|
||||||
|
}
|
||||||
|
fn, err := s.repo.Queries().GetFunction(ctx, uuid.UUID(hook.FunctionID.Bytes))
|
||||||
|
if err != nil {
|
||||||
|
return InboundReceiveResult{}, err
|
||||||
|
}
|
||||||
|
raw := body
|
||||||
|
if len(raw) == 0 {
|
||||||
|
raw = []byte(`{}`)
|
||||||
|
}
|
||||||
|
// Detached: the public endpoint acks immediately; the run row records
|
||||||
|
// the outcome. Hook callers must not wait on user code.
|
||||||
|
go func() {
|
||||||
|
runCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := s.executeFunction(runCtx, fn, raw, "hook"); err != nil && s.logger != nil {
|
||||||
|
s.logger.Warn("hook function invoke failed", "hook", hook.Name, "function", fn.Name, "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return InboundReceiveResult{Received: true, Event: "function.invoked"}, nil
|
||||||
default:
|
default:
|
||||||
s.publishEvent(ctx, hook.ProjectID, WebhookEventInboundReceived, map[string]any{
|
s.publishEvent(ctx, hook.ProjectID, WebhookEventInboundReceived, map[string]any{
|
||||||
"hook_id": hook.ID.String(),
|
"hook_id": hook.ID.String(),
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ type ScheduledJobSummary struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Schedule string `json:"schedule"`
|
Schedule string `json:"schedule"`
|
||||||
URL string `json:"url"`
|
URL string `json:"url"`
|
||||||
|
FunctionID *string `json:"function_id,omitempty"`
|
||||||
Payload json.RawMessage `json:"payload"`
|
Payload json.RawMessage `json:"payload"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
HasSecret bool `json:"has_secret"`
|
HasSecret bool `json:"has_secret"`
|
||||||
@@ -59,21 +60,23 @@ type ScheduledJobRunSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateScheduledJobInput struct {
|
type CreateScheduledJobInput struct {
|
||||||
Name string `json:"name" validate:"required,min=2"`
|
Name string `json:"name" validate:"required,min=2"`
|
||||||
Schedule string `json:"schedule" validate:"required"`
|
Schedule string `json:"schedule" validate:"required"`
|
||||||
URL string `json:"url" validate:"required,url"`
|
URL string `json:"url" validate:"required_without=FunctionID,omitempty,url"`
|
||||||
Secret string `json:"secret"`
|
FunctionID string `json:"function_id" validate:"omitempty,uuid"`
|
||||||
Payload map[string]any `json:"payload"`
|
Secret string `json:"secret"`
|
||||||
Enabled *bool `json:"enabled"`
|
Payload map[string]any `json:"payload"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateScheduledJobInput struct {
|
type UpdateScheduledJobInput struct {
|
||||||
Name *string `json:"name" validate:"omitempty,min=2"`
|
Name *string `json:"name" validate:"omitempty,min=2"`
|
||||||
Schedule *string `json:"schedule"`
|
Schedule *string `json:"schedule"`
|
||||||
URL *string `json:"url" validate:"omitempty,url"`
|
URL *string `json:"url" validate:"omitempty,url"`
|
||||||
Secret *string `json:"secret"`
|
FunctionID *string `json:"function_id" validate:"omitempty,uuid"`
|
||||||
Payload map[string]any `json:"payload"`
|
Secret *string `json:"secret"`
|
||||||
Enabled *bool `json:"enabled"`
|
Payload map[string]any `json:"payload"`
|
||||||
|
Enabled *bool `json:"enabled"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func toJobSummary(row db.CoreScheduledJob) ScheduledJobSummary {
|
func toJobSummary(row db.CoreScheduledJob) ScheduledJobSummary {
|
||||||
@@ -88,6 +91,10 @@ func toJobSummary(row db.CoreScheduledJob) ScheduledJobSummary {
|
|||||||
HasSecret: len(row.Secret) > 0,
|
HasSecret: len(row.Secret) > 0,
|
||||||
CreatedAt: row.CreatedAt.Time,
|
CreatedAt: row.CreatedAt.Time,
|
||||||
}
|
}
|
||||||
|
if row.FunctionID.Valid {
|
||||||
|
id := uuid.UUID(row.FunctionID.Bytes).String()
|
||||||
|
out.FunctionID = &id
|
||||||
|
}
|
||||||
if row.LastRunAt.Valid {
|
if row.LastRunAt.Valid {
|
||||||
t := row.LastRunAt.Time
|
t := row.LastRunAt.Time
|
||||||
out.LastRunAt = &t
|
out.LastRunAt = &t
|
||||||
@@ -176,9 +183,25 @@ func (s *PlatformService) CreateScheduledJob(ctx context.Context, actor *models.
|
|||||||
if err := s.requireProjectRole(ctx, actor, projectID, "admin", "developer"); err != nil {
|
if err := s.requireProjectRole(ctx, actor, projectID, "admin", "developer"); err != nil {
|
||||||
return ScheduledJobSummary{}, err
|
return ScheduledJobSummary{}, err
|
||||||
}
|
}
|
||||||
jobURL, err := normalizeWebhookURL(input.URL)
|
jobURL := ""
|
||||||
if err != nil {
|
var functionID pgtype.UUID
|
||||||
return ScheduledJobSummary{}, err
|
if input.FunctionID != "" {
|
||||||
|
fid, err := uuid.Parse(input.FunctionID)
|
||||||
|
if err != nil {
|
||||||
|
return ScheduledJobSummary{}, fmt.Errorf("invalid function_id")
|
||||||
|
}
|
||||||
|
fn, err := s.repo.Queries().GetFunction(ctx, fid)
|
||||||
|
if err != nil || fn.ProjectID != projectID {
|
||||||
|
return ScheduledJobSummary{}, fmt.Errorf("function not found in this project")
|
||||||
|
}
|
||||||
|
functionID = pgtype.UUID{Bytes: fid, Valid: true}
|
||||||
|
jobURL = "function://" + fid.String()
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
jobURL, err = normalizeWebhookURL(input.URL)
|
||||||
|
if err != nil {
|
||||||
|
return ScheduledJobSummary{}, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sched, err := parseSchedule(input.Schedule)
|
sched, err := parseSchedule(input.Schedule)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -222,6 +245,7 @@ func (s *PlatformService) CreateScheduledJob(ctx context.Context, actor *models.
|
|||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
NextRunAt: next,
|
NextRunAt: next,
|
||||||
CreatedByUserID: createdBy,
|
CreatedByUserID: createdBy,
|
||||||
|
FunctionID: functionID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "scheduled_jobs_project_id_name_key") {
|
if strings.Contains(err.Error(), "scheduled_jobs_project_id_name_key") {
|
||||||
@@ -260,6 +284,28 @@ func (s *PlatformService) UpdateScheduledJob(ctx context.Context, actor *models.
|
|||||||
return ScheduledJobSummary{}, err
|
return ScheduledJobSummary{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
rotateFunction := input.FunctionID != nil
|
||||||
|
functionID := row.FunctionID
|
||||||
|
if rotateFunction {
|
||||||
|
raw := strings.TrimSpace(*input.FunctionID)
|
||||||
|
if raw == "" {
|
||||||
|
functionID = pgtype.UUID{} // explicit clear → back to URL delivery
|
||||||
|
if input.URL == nil && strings.HasPrefix(jobURL, "function://") {
|
||||||
|
return ScheduledJobSummary{}, fmt.Errorf("clearing function_id requires a url")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fid, ferr := uuid.Parse(raw)
|
||||||
|
if ferr != nil {
|
||||||
|
return ScheduledJobSummary{}, fmt.Errorf("invalid function_id")
|
||||||
|
}
|
||||||
|
fn, ferr := s.repo.Queries().GetFunction(ctx, fid)
|
||||||
|
if ferr != nil || fn.ProjectID != projectID {
|
||||||
|
return ScheduledJobSummary{}, fmt.Errorf("function not found in this project")
|
||||||
|
}
|
||||||
|
functionID = pgtype.UUID{Bytes: fid, Valid: true}
|
||||||
|
jobURL = "function://" + fid.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
enabled := row.Enabled
|
enabled := row.Enabled
|
||||||
if input.Enabled != nil {
|
if input.Enabled != nil {
|
||||||
enabled = *input.Enabled
|
enabled = *input.Enabled
|
||||||
@@ -302,6 +348,8 @@ func (s *PlatformService) UpdateScheduledJob(ctx context.Context, actor *models.
|
|||||||
Secret: secretBytes,
|
Secret: secretBytes,
|
||||||
Column10: rotatePayload,
|
Column10: rotatePayload,
|
||||||
Payload: payload,
|
Payload: payload,
|
||||||
|
Column12: rotateFunction,
|
||||||
|
FunctionID: functionID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.Contains(err.Error(), "scheduled_jobs_project_id_name_key") {
|
if strings.Contains(err.Error(), "scheduled_jobs_project_id_name_key") {
|
||||||
@@ -410,6 +458,10 @@ type JobScheduler struct {
|
|||||||
leader *leaderState
|
leader *leaderState
|
||||||
// onEvent fans a finished run out to webhooks and the realtime stream.
|
// onEvent fans a finished run out to webhooks and the realtime stream.
|
||||||
onEvent func(ctx context.Context, projectID uuid.UUID, eventType string, data map[string]any)
|
onEvent func(ctx context.Context, projectID uuid.UUID, eventType string, data map[string]any)
|
||||||
|
// runFn delivers to a function instead of HTTP when job.FunctionID is set.
|
||||||
|
// The function receives the resolved job payload, not the run envelope.
|
||||||
|
// Owned by PlatformService — the scheduler stays transport-agnostic.
|
||||||
|
runFn func(ctx context.Context, job db.CoreScheduledJob, run db.CoreScheduledJobRun, payload json.RawMessage) (*int32, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type jobExec struct {
|
type jobExec struct {
|
||||||
@@ -559,7 +611,8 @@ func (j *JobScheduler) markRan(ctx context.Context, job db.CoreScheduledJob, sta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliver POSTs the job payload to its URL, signed like a webhook delivery.
|
// deliver POSTs the job payload to its URL — or invokes the target function
|
||||||
|
// when the job carries a function_id.
|
||||||
func (j *JobScheduler) deliver(ctx context.Context, job db.CoreScheduledJob, run db.CoreScheduledJobRun) (*int32, error) {
|
func (j *JobScheduler) deliver(ctx context.Context, job db.CoreScheduledJob, run db.CoreScheduledJobRun) (*int32, error) {
|
||||||
data, err := resolveSecretRefs(ctx, j.repo.ListProjectSecretValues, j.enc, job.ProjectID, job.Payload)
|
data, err := resolveSecretRefs(ctx, j.repo.ListProjectSecretValues, j.enc, job.ProjectID, job.Payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -575,6 +628,12 @@ func (j *JobScheduler) deliver(ctx context.Context, job db.CoreScheduledJob, run
|
|||||||
"occurred_at": time.Now().UTC().Format(time.RFC3339),
|
"occurred_at": time.Now().UTC().Format(time.RFC3339),
|
||||||
"data": data,
|
"data": data,
|
||||||
})
|
})
|
||||||
|
if job.FunctionID.Valid {
|
||||||
|
if j.runFn == nil {
|
||||||
|
return nil, fmt.Errorf("functions runtime not configured")
|
||||||
|
}
|
||||||
|
return j.runFn(ctx, job, run, data)
|
||||||
|
}
|
||||||
secret := ""
|
secret := ""
|
||||||
if len(job.Secret) > 0 && j.enc != nil {
|
if len(job.Secret) > 0 && j.enc != nil {
|
||||||
if plain, err := j.enc.Decrypt(job.Secret); err == nil {
|
if plain, err := j.enc.Decrypt(job.Secret); err == nil {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type PlatformService struct {
|
|||||||
scheduler *JobScheduler
|
scheduler *JobScheduler
|
||||||
alerts *alertEvaluator
|
alerts *alertEvaluator
|
||||||
functions FunctionRunner
|
functions FunctionRunner
|
||||||
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
type BootstrapInput struct {
|
type BootstrapInput struct {
|
||||||
@@ -226,11 +227,12 @@ type InvitationSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewPlatformService(repo *repositories.CoreRepository, store storage.Store, mailer *Mailer, publicURL string, dbxClient *dbx.Client, enc *secrets.Encryptor, logger *slog.Logger, functions FunctionRunner) *PlatformService {
|
func NewPlatformService(repo *repositories.CoreRepository, store storage.Store, mailer *Mailer, publicURL string, dbxClient *dbx.Client, enc *secrets.Encryptor, logger *slog.Logger, functions FunctionRunner) *PlatformService {
|
||||||
s := &PlatformService{repo: repo, store: store, mailer: mailer, publicURL: publicURL, dbx: dbxClient, hub: NewEventHub(), realtime: NewEventHub(), enc: enc, functions: functions}
|
s := &PlatformService{repo: repo, store: store, mailer: mailer, publicURL: publicURL, dbx: dbxClient, hub: NewEventHub(), realtime: NewEventHub(), enc: enc, functions: functions, logger: logger}
|
||||||
if enc != nil {
|
if enc != nil {
|
||||||
s.dispatcher = NewWebhookDispatcher(repo.Queries(), enc, logger)
|
s.dispatcher = NewWebhookDispatcher(repo.Queries(), enc, logger)
|
||||||
s.dispatcher.Start(context.Background())
|
s.dispatcher.Start(context.Background())
|
||||||
s.scheduler = NewJobScheduler(repo.Queries(), enc, logger, s.publishEvent, repo.Pool())
|
s.scheduler = NewJobScheduler(repo.Queries(), enc, logger, s.publishEvent, repo.Pool())
|
||||||
|
s.scheduler.runFn = s.runJobFunction
|
||||||
s.scheduler.Start(context.Background())
|
s.scheduler.Start(context.Background())
|
||||||
s.alerts = newAlertEvaluator(repo.Queries(), s.publishEvent, logger, repo.Pool())
|
s.alerts = newAlertEvaluator(repo.Queries(), s.publishEvent, logger, repo.Pool())
|
||||||
s.alerts.Start(context.Background())
|
s.alerts.Start(context.Background())
|
||||||
@@ -327,6 +329,7 @@ func (s *PlatformService) publishEvent(ctx context.Context, projectID uuid.UUID,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
s.dispatchEvent(ctx, projectID, eventType, data)
|
s.dispatchEvent(ctx, projectID, eventType, data)
|
||||||
|
s.triggerFunctionsForEvent(ctx, projectID, eventType, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubscribeRealtime registers an SSE consumer for project domain events.
|
// SubscribeRealtime registers an SSE consumer for project domain events.
|
||||||
|
|||||||
@@ -4058,6 +4058,11 @@ components:
|
|||||||
description: Cron expression (5-field) or descriptor (@hourly, @every 30m).
|
description: Cron expression (5-field) or descriptor (@hourly, @every 30m).
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
|
function_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
description: When set, the schedule invokes this function instead of POSTing url.
|
||||||
payload:
|
payload:
|
||||||
type: object
|
type: object
|
||||||
description: Static JSON payload merged into every delivery's data field.
|
description: Static JSON payload merged into every delivery's data field.
|
||||||
@@ -4093,6 +4098,11 @@ components:
|
|||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
description: Delivery target. Required unless function_id is set.
|
||||||
|
function_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
description: Invoke this function on schedule instead of POSTing url.
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
description: HMAC key for X-Primora-Signature. Generated when omitted.
|
description: HMAC key for X-Primora-Signature. Generated when omitted.
|
||||||
@@ -4102,7 +4112,7 @@ components:
|
|||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: true
|
default: true
|
||||||
required: [name, schedule, url]
|
required: [name, schedule]
|
||||||
UpdateScheduledJobRequest:
|
UpdateScheduledJobRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -4114,6 +4124,11 @@ components:
|
|||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
format: uri
|
format: uri
|
||||||
|
function_id:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
|
description: Set to retarget at a function; send "" to clear back to url delivery.
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
description: Replace the signing secret. Omit to keep the current one.
|
description: Replace the signing secret. Omit to keep the current one.
|
||||||
@@ -4309,9 +4324,11 @@ components:
|
|||||||
description: Full ingest URL — POST here from the external system.
|
description: Full ingest URL — POST here from the external system.
|
||||||
mode:
|
mode:
|
||||||
type: string
|
type: string
|
||||||
enum: [event, job]
|
enum: [event, job, function]
|
||||||
job_id:
|
job_id:
|
||||||
type: string
|
type: string
|
||||||
|
function_id:
|
||||||
|
type: string
|
||||||
has_secret:
|
has_secret:
|
||||||
type: boolean
|
type: boolean
|
||||||
enabled:
|
enabled:
|
||||||
@@ -4339,11 +4356,14 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
mode:
|
mode:
|
||||||
type: string
|
type: string
|
||||||
enum: [event, job]
|
enum: [event, job, function]
|
||||||
default: event
|
default: event
|
||||||
job_id:
|
job_id:
|
||||||
type: string
|
type: string
|
||||||
description: Required when mode=job — scheduled job to trigger.
|
description: Required when mode=job — scheduled job to trigger.
|
||||||
|
function_id:
|
||||||
|
type: string
|
||||||
|
description: Required when mode=function — invoked with the received body.
|
||||||
secret:
|
secret:
|
||||||
type: string
|
type: string
|
||||||
description: Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.
|
description: Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.
|
||||||
@@ -4402,6 +4422,9 @@ components:
|
|||||||
enum: [bun, deno]
|
enum: [bun, deno]
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
event_pattern:
|
||||||
|
type: string
|
||||||
|
description: 'Domain-event pattern that triggers this function — "document.*", "*", exact type. Empty = never.'
|
||||||
created_at:
|
created_at:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
@@ -4436,6 +4459,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum: [bun, deno]
|
enum: [bun, deno]
|
||||||
default: bun
|
default: bun
|
||||||
|
event_pattern:
|
||||||
|
type: string
|
||||||
|
description: 'Optional — "document.*", "*", or an exact event type.'
|
||||||
required: [name, code]
|
required: [name, code]
|
||||||
UpdateFunctionRequest:
|
UpdateFunctionRequest:
|
||||||
type: object
|
type: object
|
||||||
@@ -4444,6 +4470,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
|
event_pattern:
|
||||||
|
type: string
|
||||||
|
description: Replace the trigger pattern; "" disables event triggers.
|
||||||
InvokeFunctionRequest:
|
InvokeFunctionRequest:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -4456,7 +4485,7 @@ components:
|
|||||||
format: uuid
|
format: uuid
|
||||||
trigger:
|
trigger:
|
||||||
type: string
|
type: string
|
||||||
enum: [manual, schedule, hook]
|
enum: [manual, schedule, hook, event]
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
enum: [success, error, timeout]
|
enum: [success, error, timeout]
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js";
|
import { For, Show, createEffect, createSignal, onCleanup } from "solid-js";
|
||||||
import {
|
import {
|
||||||
AutomationService,
|
AutomationService,
|
||||||
|
FunctionsService,
|
||||||
OpenAPI,
|
OpenAPI,
|
||||||
type ScheduledJob,
|
type ScheduledJob,
|
||||||
type ScheduledJobRun,
|
type ScheduledJobRun,
|
||||||
|
type Function as PrimoraFunction,
|
||||||
} from "@primora/api-client";
|
} from "@primora/api-client";
|
||||||
import { fetchApiToken } from "../lib/auth-client";
|
import { fetchApiToken } from "../lib/auth-client";
|
||||||
import { demoService } from "../lib/demo-mode";
|
import { demoService } from "../lib/demo-mode";
|
||||||
@@ -63,7 +65,8 @@ export function AutomationPage(props: AutomationPageProps) {
|
|||||||
const [runs, setRuns] = createSignal<Record<string, ScheduledJobRun[]>>({});
|
const [runs, setRuns] = createSignal<Record<string, ScheduledJobRun[]>>({});
|
||||||
const [expandedJob, setExpandedJob] = createSignal<string>();
|
const [expandedJob, setExpandedJob] = createSignal<string>();
|
||||||
const [showCreate, setShowCreate] = createSignal(false);
|
const [showCreate, setShowCreate] = createSignal(false);
|
||||||
const [jobForm, setJobForm] = createSignal({ name: "", schedule: "", url: "", payload: "", secret: "", enabled: true });
|
const [jobForm, setJobForm] = createSignal({ name: "", schedule: "", url: "", payload: "", secret: "", enabled: true, target: "url" as "url" | "function", function_id: "" });
|
||||||
|
const [functions, setFunctions] = createSignal<PrimoraFunction[]>([]);
|
||||||
const [createdSecret, setCreatedSecret] = createSignal("");
|
const [createdSecret, setCreatedSecret] = createSignal("");
|
||||||
const [loading, setLoading] = createSignal(false);
|
const [loading, setLoading] = createSignal(false);
|
||||||
const [busy, setBusy] = createSignal<string>();
|
const [busy, setBusy] = createSignal<string>();
|
||||||
@@ -82,6 +85,10 @@ export function AutomationPage(props: AutomationPageProps) {
|
|||||||
try {
|
try {
|
||||||
const res = await svc(props.demoMode).listScheduledJobs({ projectId: props.projectID });
|
const res = await svc(props.demoMode).listScheduledJobs({ projectId: props.projectID });
|
||||||
setJobs(res.items ?? []);
|
setJobs(res.items ?? []);
|
||||||
|
if (!props.demoMode) {
|
||||||
|
const fr = await FunctionsService.listFunctions({ projectId: props.projectID });
|
||||||
|
setFunctions(fr.items ?? []);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(err(e));
|
setError(err(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -199,14 +206,15 @@ export function AutomationPage(props: AutomationPageProps) {
|
|||||||
requestBody: {
|
requestBody: {
|
||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
schedule: form.schedule.trim(),
|
schedule: form.schedule.trim(),
|
||||||
url: form.url.trim(),
|
url: form.target === "url" ? form.url.trim() : undefined,
|
||||||
|
function_id: form.target === "function" ? form.function_id : undefined,
|
||||||
secret: form.secret.trim() || undefined,
|
secret: form.secret.trim() || undefined,
|
||||||
payload,
|
payload,
|
||||||
enabled: form.enabled,
|
enabled: form.enabled,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (created.secret) setCreatedSecret(created.secret);
|
if (created.secret) setCreatedSecret(created.secret);
|
||||||
setJobForm({ name: "", schedule: "", url: "", payload: "", secret: "", enabled: true });
|
setJobForm({ name: "", schedule: "", url: "", payload: "", secret: "", enabled: true, target: "url", function_id: "" });
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
}, "Schedule created");
|
}, "Schedule created");
|
||||||
};
|
};
|
||||||
@@ -335,7 +343,9 @@ export function AutomationPage(props: AutomationPageProps) {
|
|||||||
<code class="text-xs">{job.schedule}</code>
|
<code class="text-xs">{job.schedule}</code>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-text-2" style="max-width:14rem;overflow:hidden;text-overflow:ellipsis">
|
<td class="text-text-2" style="max-width:14rem;overflow:hidden;text-overflow:ellipsis">
|
||||||
{job.url}
|
{job.function_id
|
||||||
|
? `ƒ ${functions().find((f) => f.id === job.function_id)?.name ?? "function"}`
|
||||||
|
: job.url}
|
||||||
</td>
|
</td>
|
||||||
<td class="hidden md:table-cell text-text-2">{job.enabled ? fmtTime(job.next_run_at) : "—"}</td>
|
<td class="hidden md:table-cell text-text-2">{job.enabled ? fmtTime(job.next_run_at) : "—"}</td>
|
||||||
<td class="hidden md:table-cell">
|
<td class="hidden md:table-cell">
|
||||||
@@ -545,16 +555,56 @@ export function AutomationPage(props: AutomationPageProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Input
|
<span class="label">Target</span>
|
||||||
label="Target URL"
|
<div class="flex gap-2 mb-2">
|
||||||
value={jobForm().url}
|
<button
|
||||||
onInput={(e) => setJobForm((c) => ({ ...c, url: e.currentTarget.value }))}
|
type="button"
|
||||||
placeholder="https://api.example.com/jobs/rollup"
|
class={`btn btn-sm ${jobForm().target === "url" ? "btn-primary" : "btn-secondary"}`}
|
||||||
required
|
onClick={() => setJobForm((c) => ({ ...c, target: "url" }))}
|
||||||
/>
|
>
|
||||||
<p class="label-hint">
|
Webhook URL
|
||||||
HTTPS for public endpoints; plain HTTP allowed for private/self-hosted targets.
|
</button>
|
||||||
</p>
|
<button
|
||||||
|
type="button"
|
||||||
|
class={`btn btn-sm ${jobForm().target === "function" ? "btn-primary" : "btn-secondary"}`}
|
||||||
|
onClick={() => setJobForm((c) => ({ ...c, target: "function" }))}
|
||||||
|
>
|
||||||
|
Function
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={jobForm().target === "function"}
|
||||||
|
fallback={
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
label="Target URL"
|
||||||
|
value={jobForm().url}
|
||||||
|
onInput={(e) => setJobForm((c) => ({ ...c, url: e.currentTarget.value }))}
|
||||||
|
placeholder="https://api.example.com/jobs/rollup"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p class="label-hint">
|
||||||
|
HTTPS for public endpoints; plain HTTP allowed for private/self-hosted targets.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
class="input"
|
||||||
|
value={jobForm().function_id}
|
||||||
|
onChange={(e) => setJobForm((c) => ({ ...c, function_id: e.currentTarget.value }))}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="">Select a function…</option>
|
||||||
|
<For each={functions()}>
|
||||||
|
{(f) => <option value={f.id}>{f.name} ({f.runtime})</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
|
<p class="label-hint">
|
||||||
|
The function receives the resolved payload as JSON. Runs appear under the function's
|
||||||
|
history.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Payload (optional JSON)"
|
label="Payload (optional JSON)"
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
const [busy, setBusy] = createSignal<string>();
|
const [busy, setBusy] = createSignal<string>();
|
||||||
const [showCreate, setShowCreate] = createSignal(false);
|
const [showCreate, setShowCreate] = createSignal(false);
|
||||||
const [createForm, setCreateForm] = createSignal({ name: "", runtime: "bun" });
|
const [createForm, setCreateForm] = createSignal({ name: "", runtime: "bun", event_pattern: "" });
|
||||||
|
const [eventPattern, setEventPattern] = createSignal("");
|
||||||
|
const [patternDirty, setPatternDirty] = createSignal(false);
|
||||||
|
|
||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (!props.projectID) return;
|
if (!props.projectID) return;
|
||||||
@@ -71,6 +73,8 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
const select = async (fn: Function) => {
|
const select = async (fn: Function) => {
|
||||||
setSelected(fn);
|
setSelected(fn);
|
||||||
setCodeDirty(false);
|
setCodeDirty(false);
|
||||||
|
setEventPattern(fn.event_pattern ?? "");
|
||||||
|
setPatternDirty(false);
|
||||||
setLastRun(undefined);
|
setLastRun(undefined);
|
||||||
if (props.demoMode) {
|
if (props.demoMode) {
|
||||||
setCode(STARTER_CODE);
|
setCode(STARTER_CODE);
|
||||||
@@ -94,10 +98,15 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
try {
|
try {
|
||||||
const fn = await FunctionsService.createFunction({
|
const fn = await FunctionsService.createFunction({
|
||||||
projectId: props.projectID,
|
projectId: props.projectID,
|
||||||
requestBody: { name: createForm().name, runtime: createForm().runtime as CreateFunctionRequest.runtime, code: STARTER_CODE },
|
requestBody: {
|
||||||
|
name: createForm().name,
|
||||||
|
runtime: createForm().runtime as CreateFunctionRequest.runtime,
|
||||||
|
code: STARTER_CODE,
|
||||||
|
event_pattern: createForm().event_pattern.trim() || undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
setShowCreate(false);
|
setShowCreate(false);
|
||||||
setCreateForm({ name: "", runtime: "bun" });
|
setCreateForm({ name: "", runtime: "bun", event_pattern: "" });
|
||||||
setMessage(`Function "${fn.name}" created.`);
|
setMessage(`Function "${fn.name}" created.`);
|
||||||
await refresh();
|
await refresh();
|
||||||
await select(fn);
|
await select(fn);
|
||||||
@@ -113,8 +122,15 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
if (!fn || !props.projectID || props.demoMode) return;
|
if (!fn || !props.projectID || props.demoMode) return;
|
||||||
setBusy("save");
|
setBusy("save");
|
||||||
try {
|
try {
|
||||||
await FunctionsService.updateFunction({ projectId: props.projectID, functionId: fn.id, requestBody: { code: code() } });
|
await FunctionsService.updateFunction({
|
||||||
|
projectId: props.projectID,
|
||||||
|
functionId: fn.id,
|
||||||
|
requestBody: { code: code(), event_pattern: eventPattern().trim() },
|
||||||
|
});
|
||||||
setCodeDirty(false);
|
setCodeDirty(false);
|
||||||
|
setPatternDirty(false);
|
||||||
|
setSelected({ ...fn, event_pattern: eventPattern().trim() });
|
||||||
|
setFunctions((list) => list.map((f) => (f.id === fn.id ? { ...f, event_pattern: eventPattern().trim() } : f)));
|
||||||
setMessage(`Saved "${fn.name}".`);
|
setMessage(`Saved "${fn.name}".`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(err(e));
|
setError(err(e));
|
||||||
@@ -286,10 +302,13 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
<div class="flex items-center justify-between" style="width:100%">
|
<div class="flex items-center justify-between" style="width:100%">
|
||||||
<span class="card-header-title">
|
<span class="card-header-title">
|
||||||
{fn().name} <Badge variant="neutral">{fn().runtime}</Badge>
|
{fn().name} <Badge variant="neutral">{fn().runtime}</Badge>
|
||||||
|
<Show when={fn().event_pattern}>
|
||||||
|
<Badge variant="warning">on {fn().event_pattern}</Badge>
|
||||||
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<Show when={props.canManage && !props.demoMode}>
|
<Show when={props.canManage && !props.demoMode}>
|
||||||
<button class="btn btn-secondary btn-sm" disabled={!codeDirty() || busy() === "save"} onClick={() => void save()}>
|
<button class="btn btn-secondary btn-sm" disabled={(!codeDirty() && !patternDirty()) || busy() === "save"} onClick={() => void save()}>
|
||||||
{busy() === "save" ? "Saving…" : "Save"}
|
{busy() === "save" ? "Saving…" : "Save"}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary btn-sm" disabled={busy() === "invoke" || !fn().enabled} onClick={() => void invoke()}>
|
<button class="btn btn-primary btn-sm" disabled={busy() === "invoke" || !fn().enabled} onClick={() => void invoke()}>
|
||||||
@@ -299,6 +318,21 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="padding:1rem 1.25rem 0">
|
||||||
|
<Input
|
||||||
|
label="Event trigger (optional)"
|
||||||
|
placeholder='document.* · * · deploy.marker'
|
||||||
|
value={eventPattern()}
|
||||||
|
onInput={(e) => { setEventPattern(e.currentTarget.value); setPatternDirty(true); }}
|
||||||
|
disabled={!props.canManage || props.demoMode}
|
||||||
|
/>
|
||||||
|
<p class="text-text-3" style="font-size:0.78rem;margin-top:0.3rem">
|
||||||
|
Runs whenever a project event matching this pattern is published — e.g.{" "}
|
||||||
|
<code>document.*</code>, <code>object.created</code>, or <code>*</code> for all. The
|
||||||
|
function receives {"{ event, data, occurred_at }"}. Schedules and inbound hooks can
|
||||||
|
also target functions.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div style="padding:1rem 1.25rem;display:grid;gap:1rem;grid-template-columns:1fr 1fr">
|
<div style="padding:1rem 1.25rem;display:grid;gap:1rem;grid-template-columns:1fr 1fr">
|
||||||
<div>
|
<div>
|
||||||
<label class="text-text-3" style="font-size:0.8rem;display:block;margin-bottom:0.35rem">Code</label>
|
<label class="text-text-3" style="font-size:0.8rem;display:block;margin-bottom:0.35rem">Code</label>
|
||||||
@@ -393,6 +427,12 @@ export function FunctionsPage(props: FunctionsPageProps) {
|
|||||||
<option value="bun">bun</option>
|
<option value="bun">bun</option>
|
||||||
<option value="deno">deno</option>
|
<option value="deno">deno</option>
|
||||||
</Select>
|
</Select>
|
||||||
|
<Input
|
||||||
|
label="Event trigger (optional)"
|
||||||
|
placeholder="document.*"
|
||||||
|
value={createForm().event_pattern}
|
||||||
|
onInput={(e) => setCreateForm((f) => ({ ...f, event_pattern: e.currentTarget.value }))}
|
||||||
|
/>
|
||||||
<p class="text-text-3" style="font-size:0.8rem">
|
<p class="text-text-3" style="font-size:0.8rem">
|
||||||
Starts from a stdin-echo template — edit the code after creating.
|
Starts from a stdin-echo template — edit the code after creating.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import {
|
|||||||
AutomationService,
|
AutomationService,
|
||||||
CreateInboundHookRequest,
|
CreateInboundHookRequest,
|
||||||
CreateIntegrationRequest,
|
CreateIntegrationRequest,
|
||||||
|
FunctionsService,
|
||||||
InboundService,
|
InboundService,
|
||||||
IntegrationsService,
|
IntegrationsService,
|
||||||
UpsertAlertRuleRequest,
|
UpsertAlertRuleRequest,
|
||||||
WebhooksService,
|
WebhooksService,
|
||||||
type AlertRule,
|
type AlertRule,
|
||||||
|
type Function as PrimoraFunction,
|
||||||
type InboundHook,
|
type InboundHook,
|
||||||
type ScheduledJob,
|
type ScheduledJob,
|
||||||
type Integration,
|
type Integration,
|
||||||
@@ -74,7 +76,8 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
const [showCreateWebhook, setShowCreateWebhook] = createSignal(false);
|
const [showCreateWebhook, setShowCreateWebhook] = createSignal(false);
|
||||||
const [showCreateAlert, setShowCreateAlert] = createSignal(false);
|
const [showCreateAlert, setShowCreateAlert] = createSignal(false);
|
||||||
const [showCreateInbound, setShowCreateInbound] = 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 [copiedUrl, setCopiedUrl] = createSignal("");
|
||||||
const [alertForm, setAlertForm] = createSignal({
|
const [alertForm, setAlertForm] = createSignal({
|
||||||
name: "",
|
name: "",
|
||||||
@@ -97,18 +100,20 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
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 }),
|
intSvc(props.demoMode).listIntegrations({ projectId: props.projectID }),
|
||||||
hookSvc(props.demoMode).listWebhooks({ 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: [] }) : AlertsService.listAlertRules({ projectId: props.projectID }),
|
||||||
props.demoMode ? Promise.resolve({ items: [] }) : InboundService.listInboundHooks({ 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: [] }) : AutomationService.listScheduledJobs({ projectId: props.projectID }),
|
||||||
|
props.demoMode ? Promise.resolve({ items: [] }) : FunctionsService.listFunctions({ projectId: props.projectID }),
|
||||||
]);
|
]);
|
||||||
setIntegrations(int.items ?? []);
|
setIntegrations(int.items ?? []);
|
||||||
setWebhooks(hooks.items ?? []);
|
setWebhooks(hooks.items ?? []);
|
||||||
setAlertRules(rules.items ?? []);
|
setAlertRules(rules.items ?? []);
|
||||||
setInboundHooks(inbound.items ?? []);
|
setInboundHooks(inbound.items ?? []);
|
||||||
setScheduledJobs(jobs.items ?? []);
|
setScheduledJobs(jobs.items ?? []);
|
||||||
|
setFunctions(fns.items ?? []);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(err(e));
|
setError(err(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -268,11 +273,12 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
name: form.name.trim(),
|
name: form.name.trim(),
|
||||||
mode: form.mode,
|
mode: form.mode,
|
||||||
job_id: form.mode === CreateInboundHookRequest.mode.JOB ? form.job_id : undefined,
|
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,
|
secret: form.secret.trim() || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setShowCreateInbound(false);
|
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");
|
}, "Inbound hook created");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -706,7 +712,9 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
<tr>
|
<tr>
|
||||||
<td class="font-medium">{hook.name}</td>
|
<td class="font-medium">{hook.name}</td>
|
||||||
<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}>
|
<Show when={!hook.enabled}>
|
||||||
{" "}<Badge variant="warning">disabled</Badge>
|
{" "}<Badge variant="warning">disabled</Badge>
|
||||||
</Show>
|
</Show>
|
||||||
@@ -943,8 +951,21 @@ export function IntegrationsPage(props: IntegrationsPageProps) {
|
|||||||
options={[
|
options={[
|
||||||
{ value: "event", label: "Publish inbound.received event" },
|
{ value: "event", label: "Publish inbound.received event" },
|
||||||
{ value: "job", label: "Trigger a scheduled job" },
|
{ 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}>
|
<Show when={inboundForm().mode === CreateInboundHookRequest.mode.JOB}>
|
||||||
<Select
|
<Select
|
||||||
label="Job"
|
label="Job"
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ export type CreateFunctionRequest = {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
runtime?: CreateFunctionRequest.runtime;
|
runtime?: CreateFunctionRequest.runtime;
|
||||||
|
/**
|
||||||
|
* Optional — "document.*", "*", or an exact event type.
|
||||||
|
*/
|
||||||
|
event_pattern?: string;
|
||||||
};
|
};
|
||||||
export namespace CreateFunctionRequest {
|
export namespace CreateFunctionRequest {
|
||||||
export enum runtime {
|
export enum runtime {
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ export type CreateInboundHookRequest = {
|
|||||||
* Required when mode=job — scheduled job to trigger.
|
* Required when mode=job — scheduled job to trigger.
|
||||||
*/
|
*/
|
||||||
job_id?: string;
|
job_id?: string;
|
||||||
|
/**
|
||||||
|
* Required when mode=function — invoked with the received body.
|
||||||
|
*/
|
||||||
|
function_id?: string;
|
||||||
/**
|
/**
|
||||||
* Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.
|
* Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.
|
||||||
*/
|
*/
|
||||||
@@ -19,6 +23,7 @@ export namespace CreateInboundHookRequest {
|
|||||||
export enum mode {
|
export enum mode {
|
||||||
EVENT = 'event',
|
EVENT = 'event',
|
||||||
JOB = 'job',
|
JOB = 'job',
|
||||||
|
FUNCTION = 'function',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8,7 +8,14 @@ export type CreateScheduledJobRequest = {
|
|||||||
* Cron expression, e.g. "*15 * * * *", or descriptor like "@every 1h".
|
* Cron expression, e.g. "*15 * * * *", or descriptor like "@every 1h".
|
||||||
*/
|
*/
|
||||||
schedule: string;
|
schedule: string;
|
||||||
url: string;
|
/**
|
||||||
|
* Delivery target. Required unless function_id is set.
|
||||||
|
*/
|
||||||
|
url?: string;
|
||||||
|
/**
|
||||||
|
* Invoke this function on schedule instead of POSTing url.
|
||||||
|
*/
|
||||||
|
function_id?: string;
|
||||||
/**
|
/**
|
||||||
* HMAC key for X-Primora-Signature. Generated when omitted.
|
* HMAC key for X-Primora-Signature. Generated when omitted.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ export type Function = {
|
|||||||
name: string;
|
name: string;
|
||||||
runtime: Function.runtime;
|
runtime: Function.runtime;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/**
|
||||||
|
* Domain-event pattern that triggers this function — "document.*", "*", exact type. Empty = never.
|
||||||
|
*/
|
||||||
|
event_pattern?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export namespace FunctionRun {
|
|||||||
MANUAL = 'manual',
|
MANUAL = 'manual',
|
||||||
SCHEDULE = 'schedule',
|
SCHEDULE = 'schedule',
|
||||||
HOOK = 'hook',
|
HOOK = 'hook',
|
||||||
|
EVENT = 'event',
|
||||||
}
|
}
|
||||||
export enum status {
|
export enum status {
|
||||||
SUCCESS = 'success',
|
SUCCESS = 'success',
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type InboundHook = {
|
|||||||
url: string;
|
url: string;
|
||||||
mode: InboundHook.mode;
|
mode: InboundHook.mode;
|
||||||
job_id?: string;
|
job_id?: string;
|
||||||
|
function_id?: string;
|
||||||
has_secret: boolean;
|
has_secret: boolean;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
last_received_at?: string | null;
|
last_received_at?: string | null;
|
||||||
@@ -22,6 +23,7 @@ export namespace InboundHook {
|
|||||||
export enum mode {
|
export enum mode {
|
||||||
EVENT = 'event',
|
EVENT = 'event',
|
||||||
JOB = 'job',
|
JOB = 'job',
|
||||||
|
FUNCTION = 'function',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,10 @@ export type ScheduledJob = {
|
|||||||
*/
|
*/
|
||||||
schedule: string;
|
schedule: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
/**
|
||||||
|
* When set, the schedule invokes this function instead of POSTing url.
|
||||||
|
*/
|
||||||
|
function_id?: string | null;
|
||||||
/**
|
/**
|
||||||
* Static JSON payload merged into every delivery's data field.
|
* Static JSON payload merged into every delivery's data field.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,5 +5,9 @@
|
|||||||
export type UpdateFunctionRequest = {
|
export type UpdateFunctionRequest = {
|
||||||
code?: string;
|
code?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
/**
|
||||||
|
* Replace the trigger pattern; "" disables event triggers.
|
||||||
|
*/
|
||||||
|
event_pattern?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -6,6 +6,10 @@ export type UpdateScheduledJobRequest = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
schedule?: string;
|
schedule?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
/**
|
||||||
|
* Set to retarget at a function; send "" to clear back to url delivery.
|
||||||
|
*/
|
||||||
|
function_id?: string | null;
|
||||||
/**
|
/**
|
||||||
* Replace the signing secret. Omit to keep the current one.
|
* Replace the signing secret. Omit to keep the current one.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -15,5 +15,9 @@ export const $CreateFunctionRequest = {
|
|||||||
runtime: {
|
runtime: {
|
||||||
type: 'Enum',
|
type: 'Enum',
|
||||||
},
|
},
|
||||||
|
event_pattern: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Optional — "document.*", "*", or an exact event type.`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -15,6 +15,10 @@ export const $CreateInboundHookRequest = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
description: `Required when mode=job — scheduled job to trigger.`,
|
description: `Required when mode=job — scheduled job to trigger.`,
|
||||||
},
|
},
|
||||||
|
function_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Required when mode=function — invoked with the received body.`,
|
||||||
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: `Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.`,
|
description: `Optional — stored encrypted; callers must then sign bodies with X-Primora-Signature.`,
|
||||||
|
|||||||
@@ -16,9 +16,14 @@ export const $CreateScheduledJobRequest = {
|
|||||||
},
|
},
|
||||||
url: {
|
url: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isRequired: true,
|
description: `Delivery target. Required unless function_id is set.`,
|
||||||
format: 'uri',
|
format: 'uri',
|
||||||
},
|
},
|
||||||
|
function_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Invoke this function on schedule instead of POSTing url.`,
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: `HMAC key for X-Primora-Signature. Generated when omitted.`,
|
description: `HMAC key for X-Primora-Signature. Generated when omitted.`,
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ export const $Function = {
|
|||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
},
|
},
|
||||||
|
event_pattern: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Domain-event pattern that triggers this function — "document.*", "*", exact type. Empty = never.`,
|
||||||
|
},
|
||||||
created_at: {
|
created_at: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ export const $InboundHook = {
|
|||||||
job_id: {
|
job_id: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
function_id: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
has_secret: {
|
has_secret: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ export const $ScheduledJob = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
},
|
},
|
||||||
|
function_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: `When set, the schedule invokes this function instead of POSTing url.`,
|
||||||
|
isNullable: true,
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
payload: {
|
payload: {
|
||||||
type: 'dictionary',
|
type: 'dictionary',
|
||||||
contains: {
|
contains: {
|
||||||
|
|||||||
@@ -10,5 +10,9 @@ export const $UpdateFunctionRequest = {
|
|||||||
enabled: {
|
enabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
event_pattern: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Replace the trigger pattern; "" disables event triggers.`,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
@@ -15,6 +15,12 @@ export const $UpdateScheduledJobRequest = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
format: 'uri',
|
format: 'uri',
|
||||||
},
|
},
|
||||||
|
function_id: {
|
||||||
|
type: 'string',
|
||||||
|
description: `Set to retarget at a function; send "" to clear back to url delivery.`,
|
||||||
|
isNullable: true,
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
secret: {
|
secret: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: `Replace the signing secret. Omit to keep the current one.`,
|
description: `Replace the signing secret. Omit to keep the current one.`,
|
||||||
|
|||||||
Reference in new issue
Block a user