mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
feat: phase 7 automation & realtime — scheduled jobs, domain events, SSE
Scheduled jobs (migration 00009): cron expressions + @every descriptors, in-process scheduler (15s scan, missed-run collapse), signed HTTP POST delivery (X-Primora-Signature), AES-256-GCM sealed secrets returned once, run history with status/duration/http-code, manual execution, retention pruning. Viewer reads, admin/developer writes. Domain events: publishEvent fans document/object/issue/deploy/job mutations to realtime subscribers and matching webhooks through one path. Webhook event filter extended to job.run, document.*, object.*. Realtime: GET /projects/:id/realtime/stream SSE with 25s keepalives and ?token=/?api_key= auth for EventSource clients. Automation dashboard page: Schedules tab (CRUD, run history, manual run, secret-once banner) + Live events tab (live badge, pause/clear). Demo mode parity. primora jobs:* CLI group, six primora_*_job* MCP tools, AutomationService in the generated client. Fixes folded in: gzip middleware buffered SSE flushes (broke EventSource on both streams — */stream now bypasses compression); event payloads double-marshaled json.RawMessage as base64; compose hardcoded localhost trusted origins so non-default ports broke sign-in; frontend auth base URL baked an absolute localhost instead of resolving /auth on origin. Verified live: stack up on :8085, signed delivery HMAC-checked at sink, @every 30s schedule firing, run history rendering, document.created landing in the live events feed. 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
4b7fdf35ad
commit
9e9a3d5f98
49 files changed
+3555
-32
No files matched your search
@@ -0,0 +1,441 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: jobs.sql
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createScheduledJob = `-- name: CreateScheduledJob :one
|
||||
INSERT INTO core.scheduled_jobs (
|
||||
project_id,
|
||||
name,
|
||||
schedule,
|
||||
url,
|
||||
secret,
|
||||
payload,
|
||||
enabled,
|
||||
next_run_at,
|
||||
created_by_user_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
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
|
||||
`
|
||||
|
||||
type CreateScheduledJobParams struct {
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
Name string `json:"name"`
|
||||
Schedule string `json:"schedule"`
|
||||
Url string `json:"url"`
|
||||
Secret []byte `json:"secret"`
|
||||
Payload []byte `json:"payload"`
|
||||
Enabled bool `json:"enabled"`
|
||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||
CreatedByUserID pgtype.UUID `json:"created_by_user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateScheduledJob(ctx context.Context, arg CreateScheduledJobParams) (CoreScheduledJob, error) {
|
||||
row := q.db.QueryRow(ctx, createScheduledJob,
|
||||
arg.ProjectID,
|
||||
arg.Name,
|
||||
arg.Schedule,
|
||||
arg.Url,
|
||||
arg.Secret,
|
||||
arg.Payload,
|
||||
arg.Enabled,
|
||||
arg.NextRunAt,
|
||||
arg.CreatedByUserID,
|
||||
)
|
||||
var i CoreScheduledJob
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteScheduledJob = `-- name: DeleteScheduledJob :one
|
||||
DELETE FROM core.scheduled_jobs
|
||||
WHERE id = $1
|
||||
AND project_id = $2
|
||||
RETURNING id
|
||||
`
|
||||
|
||||
type DeleteScheduledJobParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteScheduledJob(ctx context.Context, arg DeleteScheduledJobParams) (uuid.UUID, error) {
|
||||
row := q.db.QueryRow(ctx, deleteScheduledJob, arg.ID, arg.ProjectID)
|
||||
var id uuid.UUID
|
||||
err := row.Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
const finishScheduledJobRun = `-- name: FinishScheduledJobRun :one
|
||||
UPDATE core.scheduled_job_runs
|
||||
SET status = $2,
|
||||
status_code = $3,
|
||||
error = $4,
|
||||
duration_ms = $5,
|
||||
finished_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING id, job_id, status, triggered_by, status_code, error, duration_ms, created_at, finished_at
|
||||
`
|
||||
|
||||
type FinishScheduledJobRunParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Status string `json:"status"`
|
||||
StatusCode *int32 `json:"status_code"`
|
||||
Error string `json:"error"`
|
||||
DurationMs *int32 `json:"duration_ms"`
|
||||
}
|
||||
|
||||
func (q *Queries) FinishScheduledJobRun(ctx context.Context, arg FinishScheduledJobRunParams) (CoreScheduledJobRun, error) {
|
||||
row := q.db.QueryRow(ctx, finishScheduledJobRun,
|
||||
arg.ID,
|
||||
arg.Status,
|
||||
arg.StatusCode,
|
||||
arg.Error,
|
||||
arg.DurationMs,
|
||||
)
|
||||
var i CoreScheduledJobRun
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.JobID,
|
||||
&i.Status,
|
||||
&i.TriggeredBy,
|
||||
&i.StatusCode,
|
||||
&i.Error,
|
||||
&i.DurationMs,
|
||||
&i.CreatedAt,
|
||||
&i.FinishedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetScheduledJobByID(ctx context.Context, id uuid.UUID) (CoreScheduledJob, error) {
|
||||
row := q.db.QueryRow(ctx, getScheduledJobByID, id)
|
||||
var i CoreScheduledJob
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertScheduledJobRun = `-- name: InsertScheduledJobRun :one
|
||||
INSERT INTO core.scheduled_job_runs (job_id, triggered_by)
|
||||
VALUES ($1, $2)
|
||||
RETURNING id, job_id, status, triggered_by, status_code, error, duration_ms, created_at, finished_at
|
||||
`
|
||||
|
||||
type InsertScheduledJobRunParams struct {
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
TriggeredBy string `json:"triggered_by"`
|
||||
}
|
||||
|
||||
func (q *Queries) InsertScheduledJobRun(ctx context.Context, arg InsertScheduledJobRunParams) (CoreScheduledJobRun, error) {
|
||||
row := q.db.QueryRow(ctx, insertScheduledJobRun, arg.JobID, arg.TriggeredBy)
|
||||
var i CoreScheduledJobRun
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.JobID,
|
||||
&i.Status,
|
||||
&i.TriggeredBy,
|
||||
&i.StatusCode,
|
||||
&i.Error,
|
||||
&i.DurationMs,
|
||||
&i.CreatedAt,
|
||||
&i.FinishedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
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
|
||||
WHERE enabled
|
||||
AND next_run_at IS NOT NULL
|
||||
AND next_run_at <= $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListDueScheduledJobs(ctx context.Context, nextRunAt pgtype.Timestamptz) ([]CoreScheduledJob, error) {
|
||||
rows, err := q.db.Query(ctx, listDueScheduledJobs, nextRunAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []CoreScheduledJob
|
||||
for rows.Next() {
|
||||
var i CoreScheduledJob
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listScheduledJobRuns = `-- name: ListScheduledJobRuns :many
|
||||
SELECT id, job_id, status, triggered_by, status_code, error, duration_ms, created_at, finished_at FROM core.scheduled_job_runs
|
||||
WHERE job_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
type ListScheduledJobRunsParams struct {
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListScheduledJobRuns(ctx context.Context, arg ListScheduledJobRunsParams) ([]CoreScheduledJobRun, error) {
|
||||
rows, err := q.db.Query(ctx, listScheduledJobRuns, arg.JobID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []CoreScheduledJobRun
|
||||
for rows.Next() {
|
||||
var i CoreScheduledJobRun
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.JobID,
|
||||
&i.Status,
|
||||
&i.TriggeredBy,
|
||||
&i.StatusCode,
|
||||
&i.Error,
|
||||
&i.DurationMs,
|
||||
&i.CreatedAt,
|
||||
&i.FinishedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
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
|
||||
WHERE project_id = $1
|
||||
ORDER BY created_at ASC
|
||||
`
|
||||
|
||||
func (q *Queries) ListScheduledJobs(ctx context.Context, projectID uuid.UUID) ([]CoreScheduledJob, error) {
|
||||
rows, err := q.db.Query(ctx, listScheduledJobs, projectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []CoreScheduledJob
|
||||
for rows.Next() {
|
||||
var i CoreScheduledJob
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const markScheduledJobRan = `-- name: MarkScheduledJobRan :one
|
||||
UPDATE core.scheduled_jobs
|
||||
SET last_run_at = $2,
|
||||
last_status = $3,
|
||||
next_run_at = $4,
|
||||
updated_at = NOW()
|
||||
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
|
||||
`
|
||||
|
||||
type MarkScheduledJobRanParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
LastRunAt pgtype.Timestamptz `json:"last_run_at"`
|
||||
LastStatus string `json:"last_status"`
|
||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) MarkScheduledJobRan(ctx context.Context, arg MarkScheduledJobRanParams) (CoreScheduledJob, error) {
|
||||
row := q.db.QueryRow(ctx, markScheduledJobRan,
|
||||
arg.ID,
|
||||
arg.LastRunAt,
|
||||
arg.LastStatus,
|
||||
arg.NextRunAt,
|
||||
)
|
||||
var i CoreScheduledJob
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const pruneScheduledJobRuns = `-- name: PruneScheduledJobRuns :exec
|
||||
DELETE FROM core.scheduled_job_runs r
|
||||
WHERE r.job_id = $1
|
||||
AND r.id NOT IN (
|
||||
SELECT k.id FROM core.scheduled_job_runs k
|
||||
WHERE k.job_id = $1
|
||||
ORDER BY k.created_at DESC
|
||||
LIMIT $2
|
||||
)
|
||||
`
|
||||
|
||||
type PruneScheduledJobRunsParams struct {
|
||||
JobID uuid.UUID `json:"job_id"`
|
||||
Limit int32 `json:"limit"`
|
||||
}
|
||||
|
||||
func (q *Queries) PruneScheduledJobRuns(ctx context.Context, arg PruneScheduledJobRunsParams) error {
|
||||
_, err := q.db.Exec(ctx, pruneScheduledJobRuns, arg.JobID, arg.Limit)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateScheduledJob = `-- name: UpdateScheduledJob :one
|
||||
UPDATE core.scheduled_jobs
|
||||
SET name = $3,
|
||||
schedule = $4,
|
||||
url = $5,
|
||||
enabled = $6,
|
||||
next_run_at = $7,
|
||||
secret = CASE WHEN $8::boolean THEN $9 ELSE secret END,
|
||||
payload = CASE WHEN $10::boolean THEN $11 ELSE payload END,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
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
|
||||
`
|
||||
|
||||
type UpdateScheduledJobParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ProjectID uuid.UUID `json:"project_id"`
|
||||
Name string `json:"name"`
|
||||
Schedule string `json:"schedule"`
|
||||
Url string `json:"url"`
|
||||
Enabled bool `json:"enabled"`
|
||||
NextRunAt pgtype.Timestamptz `json:"next_run_at"`
|
||||
Column8 bool `json:"column_8"`
|
||||
Secret []byte `json:"secret"`
|
||||
Column10 bool `json:"column_10"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateScheduledJob(ctx context.Context, arg UpdateScheduledJobParams) (CoreScheduledJob, error) {
|
||||
row := q.db.QueryRow(ctx, updateScheduledJob,
|
||||
arg.ID,
|
||||
arg.ProjectID,
|
||||
arg.Name,
|
||||
arg.Schedule,
|
||||
arg.Url,
|
||||
arg.Enabled,
|
||||
arg.NextRunAt,
|
||||
arg.Column8,
|
||||
arg.Secret,
|
||||
arg.Column10,
|
||||
arg.Payload,
|
||||
)
|
||||
var i CoreScheduledJob
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ProjectID,
|
||||
&i.Name,
|
||||
&i.Schedule,
|
||||
&i.Url,
|
||||
&i.Secret,
|
||||
&i.Payload,
|
||||
&i.Enabled,
|
||||
&i.LastRunAt,
|
||||
&i.LastStatus,
|
||||
&i.NextRunAt,
|
||||
&i.CreatedByUserID,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
Reference in new issue
Block a user