phase 2 (part 2): sqlc queries, audit/event helpers, event NOTIFY trigger
- sqlc.yaml + internal/db/queries/*.sql: typed queries for entities, relationships, ontology, operations (signals, events, audit, idempotency, entity_status) - internal/db/sqlcgen/: generated Go from sqlc (pgx/v5) - internal/observability/record.go: Audit() and Event() helpers that write in the caller's transaction (SG10). actorLabel is interim text identity in detail JSON until OIDC resolution lands; actor_id column exists but is not yet populated - migrations/008: post-commit pg_notify trigger on events table for SSE fan-out (SG8/SG10)
This commit is contained in:
52
internal/db/queries/entities.sql
Normal file
52
internal/db/queries/entities.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- Entity read + mutation queries (API paths). Aliased `e` throughout to
|
||||
-- avoid ambiguity with joined tables.
|
||||
|
||||
-- name: GetEntityByID :one
|
||||
SELECT e.* FROM entities e WHERE e.id = $1;
|
||||
|
||||
-- name: GetEntityBySlug :one
|
||||
SELECT e.* FROM entities e WHERE e.slug = $1;
|
||||
|
||||
-- name: ListEntities :many
|
||||
WITH RECURSIVE tt AS (
|
||||
SELECT name FROM entity_types WHERE sqlc.narg('type')::text IS NULL OR name = sqlc.narg('type')
|
||||
UNION
|
||||
SELECT et.name FROM entity_types et JOIN tt ON et.parent_type = tt.name
|
||||
WHERE sqlc.narg('type')::text IS NOT NULL
|
||||
)
|
||||
SELECT e.* FROM entities e
|
||||
JOIN entity_types et ON et.name = e.type
|
||||
WHERE e.type IN (SELECT name FROM tt)
|
||||
AND (sqlc.narg('state')::text IS NULL OR e.state = sqlc.narg('state'))
|
||||
AND (sqlc.narg('domain')::text IS NULL OR et.domain = sqlc.narg('domain'))
|
||||
AND (sqlc.narg('layer')::text IS NULL OR et.layer = sqlc.narg('layer'))
|
||||
AND (sqlc.narg('q')::text IS NULL
|
||||
OR e.slug ILIKE '%'||sqlc.narg('q')||'%'
|
||||
OR e.name ILIKE '%'||sqlc.narg('q')||'%')
|
||||
AND (sqlc.narg('cursor')::text IS NULL OR e.slug > sqlc.narg('cursor'))
|
||||
ORDER BY e.slug
|
||||
LIMIT sqlc.arg('lim');
|
||||
|
||||
-- name: ListEntitiesCapped :many
|
||||
SELECT e.* FROM entities e ORDER BY e.slug LIMIT $1;
|
||||
|
||||
-- name: InsertEntity :one
|
||||
INSERT INTO entities (id, slug, type, name, state, attributes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateEntity :one
|
||||
UPDATE entities SET
|
||||
name = COALESCE(sqlc.narg('name'), name),
|
||||
state = COALESCE(sqlc.narg('state'), state),
|
||||
attributes = COALESCE(sqlc.narg('attributes'), attributes),
|
||||
maintenance_until = CASE WHEN sqlc.arg('set_maintenance')::bool
|
||||
THEN sqlc.narg('maintenance_until') ELSE maintenance_until END,
|
||||
version = version + 1,
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg('id') AND version = sqlc.arg('version')
|
||||
RETURNING *;
|
||||
|
||||
-- blast_radius(): the recursive-CTE traversal function's TABLE return type
|
||||
-- is opaque to sqlc's analyzer — that one query stays hand-written pgx in
|
||||
-- internal/httpapi (see impl.go).
|
||||
13
internal/db/queries/ontology.sql
Normal file
13
internal/db/queries/ontology.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- name: ListEntityTypes :many
|
||||
SELECT * FROM entity_types ORDER BY name;
|
||||
|
||||
-- name: ListRelationshipTypes :many
|
||||
SELECT * FROM relationship_types ORDER BY name;
|
||||
|
||||
-- name: ListLifecycleDefs :many
|
||||
SELECT * FROM lifecycle_defs ORDER BY id;
|
||||
|
||||
-- name: GetLifecycleForType :one
|
||||
SELECT ld.* FROM lifecycle_defs ld
|
||||
JOIN entity_types et ON et.lifecycle_id = ld.id
|
||||
WHERE et.name = $1;
|
||||
56
internal/db/queries/operations.sql
Normal file
56
internal/db/queries/operations.sql
Normal file
@@ -0,0 +1,56 @@
|
||||
-- name: ListSignals :many
|
||||
SELECT sig.entity_id, se.slug, sig.kind, sig.severity, sig.state,
|
||||
te.slug AS target_slug, sig.check_id, sig.evidence, sig.likely_cause,
|
||||
sig.occurrence_count, sig.flap_count, sig.hold_down_until,
|
||||
sig.mute_until, sig.first_seen_at, sig.last_seen_at
|
||||
FROM signals sig
|
||||
JOIN entities se ON se.id = sig.entity_id
|
||||
LEFT JOIN entities te ON te.id = sig.target_entity_id
|
||||
WHERE (sqlc.narg('state')::text IS NULL OR sig.state = sqlc.narg('state'))
|
||||
AND (sqlc.narg('severity')::text IS NULL OR sig.severity = sqlc.narg('severity'))
|
||||
AND (sqlc.narg('target')::text IS NULL OR te.slug = sqlc.narg('target'))
|
||||
AND (sqlc.narg('kind')::text IS NULL OR sig.kind = sqlc.narg('kind'))
|
||||
AND (sqlc.narg('cursor')::text IS NULL OR se.slug > sqlc.narg('cursor'))
|
||||
ORDER BY se.slug
|
||||
LIMIT sqlc.arg('lim');
|
||||
|
||||
-- name: ListEntityStatus :many
|
||||
SELECT e.slug, e.type, st.health, st.last_check_at
|
||||
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
||||
ORDER BY e.slug;
|
||||
|
||||
-- name: GetIdempotentResponse :one
|
||||
SELECT response_code, response_body, request_hash FROM idempotency_keys
|
||||
WHERE actor = $1 AND key = $2;
|
||||
|
||||
-- name: PutIdempotentResponse :exec
|
||||
INSERT INTO idempotency_keys (actor, key, request_hash, response_code, response_body)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (actor, key) DO NOTHING;
|
||||
|
||||
-- name: InsertAuditEntry :exec
|
||||
INSERT INTO audit_log (actor_type, actor_id, action, entity_id, method, path,
|
||||
status_code, detail, source_ip, correlation_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
|
||||
|
||||
-- name: InsertEvent :one
|
||||
INSERT INTO events (type, entity_id, severity, source, data, correlation_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, ts;
|
||||
|
||||
-- name: ListEvents :many
|
||||
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
|
||||
FROM events
|
||||
WHERE (sqlc.narg('type')::text IS NULL OR type = sqlc.narg('type'))
|
||||
AND (sqlc.narg('entity_id')::uuid IS NULL OR entity_id = sqlc.narg('entity_id'))
|
||||
AND (sqlc.narg('severity')::text IS NULL OR severity = sqlc.narg('severity'))
|
||||
AND (sqlc.narg('correlation_id')::text IS NULL OR correlation_id = sqlc.narg('correlation_id'))
|
||||
AND (sqlc.narg('from_ts')::timestamptz IS NULL OR ts >= sqlc.narg('from_ts'))
|
||||
AND (sqlc.narg('to_ts')::timestamptz IS NULL OR ts <= sqlc.narg('to_ts'))
|
||||
AND (sqlc.narg('before_id')::bigint IS NULL OR id < sqlc.narg('before_id'))
|
||||
ORDER BY id DESC
|
||||
LIMIT sqlc.arg('lim');
|
||||
|
||||
-- name: ListEventsAfter :many
|
||||
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
|
||||
FROM events WHERE id > $1 ORDER BY id ASC LIMIT $2;
|
||||
33
internal/db/queries/relationships.sql
Normal file
33
internal/db/queries/relationships.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- name: ListEntityRelations :many
|
||||
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
|
||||
r.valid_from, r.valid_to
|
||||
FROM relationships r
|
||||
JOIN entities se ON se.id = r.source_id
|
||||
JOIN entities te ON te.id = r.target_id
|
||||
WHERE r.valid_to IS NULL
|
||||
AND ((sqlc.arg('direction')::text IN ('out','both') AND r.source_id = sqlc.arg('id'))
|
||||
OR (sqlc.arg('direction')::text IN ('in','both') AND r.target_id = sqlc.arg('id')))
|
||||
AND (sqlc.narg('rel_type')::text IS NULL OR r.type = sqlc.narg('rel_type'))
|
||||
ORDER BY r.type, se.slug, te.slug;
|
||||
|
||||
-- name: ListGraphEdges :many
|
||||
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
|
||||
r.valid_from, r.valid_to
|
||||
FROM relationships r
|
||||
JOIN entities se ON se.id = r.source_id
|
||||
JOIN entities te ON te.id = r.target_id
|
||||
WHERE r.valid_to IS NULL
|
||||
AND r.source_id = ANY(sqlc.arg('ids')::uuid[])
|
||||
AND r.target_id = ANY(sqlc.arg('ids')::uuid[])
|
||||
AND (sqlc.narg('rel_types')::text[] IS NULL OR r.type = ANY(sqlc.narg('rel_types')::text[]))
|
||||
ORDER BY r.type, se.slug, te.slug;
|
||||
|
||||
-- name: UpsertCurrentRelationship :exec
|
||||
INSERT INTO relationships (source_id, target_id, type, attributes, valid_from, valid_to)
|
||||
VALUES ($1, $2, $3, $4, now(), NULL)
|
||||
ON CONFLICT (source_id, target_id, type) WHERE valid_to IS NULL
|
||||
DO UPDATE SET attributes = EXCLUDED.attributes;
|
||||
|
||||
-- name: EndCurrentRelationship :execrows
|
||||
UPDATE relationships SET valid_to = now()
|
||||
WHERE source_id = $1 AND target_id = $2 AND type = $3 AND valid_to IS NULL;
|
||||
32
internal/db/sqlcgen/db.go
Normal file
32
internal/db/sqlcgen/db.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
254
internal/db/sqlcgen/entities.sql.go
Normal file
254
internal/db/sqlcgen/entities.sql.go
Normal file
@@ -0,0 +1,254 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: entities.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getEntityByID = `-- name: GetEntityByID :one
|
||||
|
||||
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e WHERE e.id = $1
|
||||
`
|
||||
|
||||
// Entity read + mutation queries (API paths). Aliased `e` throughout to
|
||||
// avoid ambiguity with joined tables.
|
||||
func (q *Queries) GetEntityByID(ctx context.Context, id uuid.UUID) (Entity, error) {
|
||||
row := q.db.QueryRow(ctx, getEntityByID, id)
|
||||
var i Entity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getEntityBySlug = `-- name: GetEntityBySlug :one
|
||||
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e WHERE e.slug = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetEntityBySlug(ctx context.Context, slug string) (Entity, error) {
|
||||
row := q.db.QueryRow(ctx, getEntityBySlug, slug)
|
||||
var i Entity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertEntity = `-- name: InsertEntity :one
|
||||
INSERT INTO entities (id, slug, type, name, state, attributes)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at
|
||||
`
|
||||
|
||||
type InsertEntityParams struct {
|
||||
ID uuid.UUID
|
||||
Slug string
|
||||
Type string
|
||||
Name string
|
||||
State *string
|
||||
Attributes []byte
|
||||
}
|
||||
|
||||
func (q *Queries) InsertEntity(ctx context.Context, arg InsertEntityParams) (Entity, error) {
|
||||
row := q.db.QueryRow(ctx, insertEntity,
|
||||
arg.ID,
|
||||
arg.Slug,
|
||||
arg.Type,
|
||||
arg.Name,
|
||||
arg.State,
|
||||
arg.Attributes,
|
||||
)
|
||||
var i Entity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listEntities = `-- name: ListEntities :many
|
||||
WITH RECURSIVE tt AS (
|
||||
SELECT name FROM entity_types WHERE $7::text IS NULL OR name = $7
|
||||
UNION
|
||||
SELECT et.name FROM entity_types et JOIN tt ON et.parent_type = tt.name
|
||||
WHERE $7::text IS NOT NULL
|
||||
)
|
||||
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e
|
||||
JOIN entity_types et ON et.name = e.type
|
||||
WHERE e.type IN (SELECT name FROM tt)
|
||||
AND ($1::text IS NULL OR e.state = $1)
|
||||
AND ($2::text IS NULL OR et.domain = $2)
|
||||
AND ($3::text IS NULL OR et.layer = $3)
|
||||
AND ($4::text IS NULL
|
||||
OR e.slug ILIKE '%'||$4||'%'
|
||||
OR e.name ILIKE '%'||$4||'%')
|
||||
AND ($5::text IS NULL OR e.slug > $5)
|
||||
ORDER BY e.slug
|
||||
LIMIT $6
|
||||
`
|
||||
|
||||
type ListEntitiesParams struct {
|
||||
State *string
|
||||
Domain *string
|
||||
Layer *string
|
||||
Q *string
|
||||
Cursor *string
|
||||
Lim int32
|
||||
Type *string
|
||||
}
|
||||
|
||||
func (q *Queries) ListEntities(ctx context.Context, arg ListEntitiesParams) ([]Entity, error) {
|
||||
rows, err := q.db.Query(ctx, listEntities,
|
||||
arg.State,
|
||||
arg.Domain,
|
||||
arg.Layer,
|
||||
arg.Q,
|
||||
arg.Cursor,
|
||||
arg.Lim,
|
||||
arg.Type,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Entity
|
||||
for rows.Next() {
|
||||
var i Entity
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&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 listEntitiesCapped = `-- name: ListEntitiesCapped :many
|
||||
SELECT e.id, e.slug, e.type, e.name, e.state, e.attributes, e.maintenance_until, e.version, e.created_at, e.updated_at FROM entities e ORDER BY e.slug LIMIT $1
|
||||
`
|
||||
|
||||
func (q *Queries) ListEntitiesCapped(ctx context.Context, limit int32) ([]Entity, error) {
|
||||
rows, err := q.db.Query(ctx, listEntitiesCapped, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Entity
|
||||
for rows.Next() {
|
||||
var i Entity
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&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 updateEntity = `-- name: UpdateEntity :one
|
||||
UPDATE entities SET
|
||||
name = COALESCE($1, name),
|
||||
state = COALESCE($2, state),
|
||||
attributes = COALESCE($3, attributes),
|
||||
maintenance_until = CASE WHEN $4::bool
|
||||
THEN $5 ELSE maintenance_until END,
|
||||
version = version + 1,
|
||||
updated_at = now()
|
||||
WHERE id = $6 AND version = $7
|
||||
RETURNING id, slug, type, name, state, attributes, maintenance_until, version, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateEntityParams struct {
|
||||
Name *string
|
||||
State *string
|
||||
Attributes []byte
|
||||
SetMaintenance bool
|
||||
MaintenanceUntil *time.Time
|
||||
ID uuid.UUID
|
||||
Version int32
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateEntity(ctx context.Context, arg UpdateEntityParams) (Entity, error) {
|
||||
row := q.db.QueryRow(ctx, updateEntity,
|
||||
arg.Name,
|
||||
arg.State,
|
||||
arg.Attributes,
|
||||
arg.SetMaintenance,
|
||||
arg.MaintenanceUntil,
|
||||
arg.ID,
|
||||
arg.Version,
|
||||
)
|
||||
var i Entity
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Name,
|
||||
&i.State,
|
||||
&i.Attributes,
|
||||
&i.MaintenanceUntil,
|
||||
&i.Version,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
329
internal/db/sqlcgen/models.go
Normal file
329
internal/db/sqlcgen/models.go
Normal file
@@ -0,0 +1,329 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AgentActivity struct {
|
||||
ID int64
|
||||
Ts time.Time
|
||||
AgentID uuid.UUID
|
||||
SessionID *string
|
||||
ActivityType string
|
||||
ToolName *string
|
||||
EntityID *uuid.UUID
|
||||
InputSummary *string
|
||||
OutputSummary *string
|
||||
DurationMs *int32
|
||||
TokenCount *int32
|
||||
Success *bool
|
||||
CorrelationID *string
|
||||
}
|
||||
|
||||
type Approval struct {
|
||||
EntityID uuid.UUID
|
||||
SubjectEntityID *uuid.UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
Kind string
|
||||
Payload []byte
|
||||
Status string
|
||||
TokenHash *string
|
||||
ExpiresAt time.Time
|
||||
DecidedAt *time.Time
|
||||
DecidedBy *uuid.UUID
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type ApprovalRule struct {
|
||||
ID uuid.UUID
|
||||
EntityType *string
|
||||
Action string
|
||||
RiskClass string
|
||||
AutonomyLevel string
|
||||
ScopeEntity *uuid.UUID
|
||||
Version int32
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type AuditLog struct {
|
||||
ID int64
|
||||
Ts time.Time
|
||||
ActorType string
|
||||
ActorID *uuid.UUID
|
||||
Action string
|
||||
EntityID *uuid.UUID
|
||||
Method *string
|
||||
Path *string
|
||||
StatusCode *int32
|
||||
Detail []byte
|
||||
SourceIp *string
|
||||
CorrelationID *string
|
||||
}
|
||||
|
||||
type AutonomySetting struct {
|
||||
Key string
|
||||
Value string
|
||||
Version int32
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type CheckDef struct {
|
||||
EntityID uuid.UUID
|
||||
TargetID *uuid.UUID
|
||||
TargetType *string
|
||||
Kind string
|
||||
Config []byte
|
||||
IntervalS int32
|
||||
TimeoutS int32
|
||||
Zone *string
|
||||
Enabled bool
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type Classification struct {
|
||||
EntityID uuid.UUID
|
||||
SignalEntityID *uuid.UUID
|
||||
TargetEntityID *uuid.UUID
|
||||
Action string
|
||||
RecommendedAction []byte
|
||||
RiskClass string
|
||||
Route string
|
||||
BlastRadius []uuid.UUID
|
||||
PatternConfidence *float32
|
||||
SkillID *uuid.UUID
|
||||
AutonomyCheck *string
|
||||
Reasoning []byte
|
||||
CorrelationID string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Entity struct {
|
||||
ID uuid.UUID
|
||||
Slug string
|
||||
Type string
|
||||
Name string
|
||||
State *string
|
||||
Attributes []byte
|
||||
MaintenanceUntil *time.Time
|
||||
Version int32
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type EntityStatus struct {
|
||||
EntityID uuid.UUID
|
||||
Health string
|
||||
LastCheckAt *time.Time
|
||||
Details []byte
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type EntityType struct {
|
||||
Name string
|
||||
ParentType *string
|
||||
IsAbstract bool
|
||||
Domain string
|
||||
Layer string
|
||||
Description *string
|
||||
LifecycleID *string
|
||||
AttributeSchema []byte
|
||||
SchemaVersion int32
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
ID int64
|
||||
Ts time.Time
|
||||
Type string
|
||||
EntityID *uuid.UUID
|
||||
Severity string
|
||||
Source string
|
||||
Data []byte
|
||||
CorrelationID *string
|
||||
}
|
||||
|
||||
type Execution struct {
|
||||
EntityID uuid.UUID
|
||||
ClassificationID *uuid.UUID
|
||||
SignalEntityID *uuid.UUID
|
||||
TargetEntityID *uuid.UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
ApprovalID *uuid.UUID
|
||||
AgentID *uuid.UUID
|
||||
SkillID *uuid.UUID
|
||||
SkillVersion *int32
|
||||
Status string
|
||||
Result []byte
|
||||
DurationMs *int32
|
||||
Verified bool
|
||||
CorrelationID string
|
||||
StartedAt *time.Time
|
||||
CompletedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Feedback struct {
|
||||
EntityID uuid.UUID
|
||||
ExecutionID uuid.UUID
|
||||
Outcome string
|
||||
Observation *string
|
||||
Lesson *string
|
||||
UnexpectedSideEffects []string
|
||||
Tags []string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type IdempotencyKey struct {
|
||||
Key string
|
||||
Actor string
|
||||
RequestHash string
|
||||
ResponseCode *int32
|
||||
ResponseBody []byte
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Ledger struct {
|
||||
Ts time.Time
|
||||
ExecutionID uuid.UUID
|
||||
TargetEntityID *uuid.UUID
|
||||
Action string
|
||||
RiskClass string
|
||||
Status string
|
||||
Verified bool
|
||||
Route *string
|
||||
Reasoning []byte
|
||||
ApprovalStatus *string
|
||||
DecidedBy *uuid.UUID
|
||||
AgentID *uuid.UUID
|
||||
CorrelationID string
|
||||
}
|
||||
|
||||
type LifecycleDef struct {
|
||||
ID string
|
||||
States []string
|
||||
DefaultState string
|
||||
TerminalStates []string
|
||||
Transitions []byte
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type MetricRollups1d struct {
|
||||
Bucket interface{}
|
||||
EntityID uuid.UUID
|
||||
Metric string
|
||||
AvgValue float64
|
||||
MinValue interface{}
|
||||
MaxValue interface{}
|
||||
SampleCount int64
|
||||
}
|
||||
|
||||
type MetricRollups1h struct {
|
||||
Bucket interface{}
|
||||
EntityID uuid.UUID
|
||||
Metric string
|
||||
AvgValue float64
|
||||
MinValue interface{}
|
||||
MaxValue interface{}
|
||||
SampleCount int64
|
||||
}
|
||||
|
||||
type MetricSample struct {
|
||||
Ts time.Time
|
||||
EntityID uuid.UUID
|
||||
Metric string
|
||||
Value float64
|
||||
Tags []byte
|
||||
}
|
||||
|
||||
type Pattern struct {
|
||||
EntityID uuid.UUID
|
||||
AppliesType string
|
||||
Action string
|
||||
Pattern string
|
||||
Confidence float32
|
||||
EvidenceCount int32
|
||||
SuccessCount int32
|
||||
FailureCount int32
|
||||
Status string
|
||||
Quarantined bool
|
||||
Version int32
|
||||
LastValidatedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type Relationship struct {
|
||||
SourceID uuid.UUID
|
||||
TargetID uuid.UUID
|
||||
Type string
|
||||
Attributes []byte
|
||||
ValidFrom time.Time
|
||||
ValidTo *time.Time
|
||||
}
|
||||
|
||||
type RelationshipType struct {
|
||||
Name string
|
||||
Inverse *string
|
||||
SourceType string
|
||||
TargetType string
|
||||
Cardinality string
|
||||
Description *string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
type RiskClass struct {
|
||||
Name string
|
||||
Description *string
|
||||
ApprovalRequired string
|
||||
AutonomyAllowed bool
|
||||
}
|
||||
|
||||
type SeedVersion struct {
|
||||
File string
|
||||
ContentHash string
|
||||
AppliedAt time.Time
|
||||
}
|
||||
|
||||
type Signal struct {
|
||||
EntityID uuid.UUID
|
||||
Kind string
|
||||
Severity string
|
||||
TargetEntityID *uuid.UUID
|
||||
CheckID *uuid.UUID
|
||||
Evidence *string
|
||||
LikelyCause *string
|
||||
State string
|
||||
OccurrenceCount int32
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
FlapCount int32
|
||||
HoldDownUntil *time.Time
|
||||
MuteUntil *time.Time
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type Skill struct {
|
||||
EntityID uuid.UUID
|
||||
Version int32
|
||||
Name string
|
||||
Procedure []byte
|
||||
AppliesType *string
|
||||
Action string
|
||||
PatternIds []uuid.UUID
|
||||
Status string
|
||||
SuccessRate *float32
|
||||
ChangedBy *uuid.UUID
|
||||
ChangeReason *string
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
130
internal/db/sqlcgen/ontology.sql.go
Normal file
130
internal/db/sqlcgen/ontology.sql.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: ontology.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getLifecycleForType = `-- name: GetLifecycleForType :one
|
||||
SELECT ld.id, ld.states, ld.default_state, ld.terminal_states, ld.transitions, ld.created_at FROM lifecycle_defs ld
|
||||
JOIN entity_types et ON et.lifecycle_id = ld.id
|
||||
WHERE et.name = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetLifecycleForType(ctx context.Context, name string) (LifecycleDef, error) {
|
||||
row := q.db.QueryRow(ctx, getLifecycleForType, name)
|
||||
var i LifecycleDef
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.States,
|
||||
&i.DefaultState,
|
||||
&i.TerminalStates,
|
||||
&i.Transitions,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listEntityTypes = `-- name: ListEntityTypes :many
|
||||
SELECT name, parent_type, is_abstract, domain, layer, description, lifecycle_id, attribute_schema, schema_version, status, created_at, updated_at FROM entity_types ORDER BY name
|
||||
`
|
||||
|
||||
func (q *Queries) ListEntityTypes(ctx context.Context) ([]EntityType, error) {
|
||||
rows, err := q.db.Query(ctx, listEntityTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []EntityType
|
||||
for rows.Next() {
|
||||
var i EntityType
|
||||
if err := rows.Scan(
|
||||
&i.Name,
|
||||
&i.ParentType,
|
||||
&i.IsAbstract,
|
||||
&i.Domain,
|
||||
&i.Layer,
|
||||
&i.Description,
|
||||
&i.LifecycleID,
|
||||
&i.AttributeSchema,
|
||||
&i.SchemaVersion,
|
||||
&i.Status,
|
||||
&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 listLifecycleDefs = `-- name: ListLifecycleDefs :many
|
||||
SELECT id, states, default_state, terminal_states, transitions, created_at FROM lifecycle_defs ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListLifecycleDefs(ctx context.Context) ([]LifecycleDef, error) {
|
||||
rows, err := q.db.Query(ctx, listLifecycleDefs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []LifecycleDef
|
||||
for rows.Next() {
|
||||
var i LifecycleDef
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.States,
|
||||
&i.DefaultState,
|
||||
&i.TerminalStates,
|
||||
&i.Transitions,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listRelationshipTypes = `-- name: ListRelationshipTypes :many
|
||||
SELECT name, inverse, source_type, target_type, cardinality, description, created_at FROM relationship_types ORDER BY name
|
||||
`
|
||||
|
||||
func (q *Queries) ListRelationshipTypes(ctx context.Context) ([]RelationshipType, error) {
|
||||
rows, err := q.db.Query(ctx, listRelationshipTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []RelationshipType
|
||||
for rows.Next() {
|
||||
var i RelationshipType
|
||||
if err := rows.Scan(
|
||||
&i.Name,
|
||||
&i.Inverse,
|
||||
&i.SourceType,
|
||||
&i.TargetType,
|
||||
&i.Cardinality,
|
||||
&i.Description,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
357
internal/db/sqlcgen/operations.sql.go
Normal file
357
internal/db/sqlcgen/operations.sql.go
Normal file
@@ -0,0 +1,357 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: operations.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const getIdempotentResponse = `-- name: GetIdempotentResponse :one
|
||||
SELECT response_code, response_body, request_hash FROM idempotency_keys
|
||||
WHERE actor = $1 AND key = $2
|
||||
`
|
||||
|
||||
type GetIdempotentResponseParams struct {
|
||||
Actor string
|
||||
Key string
|
||||
}
|
||||
|
||||
type GetIdempotentResponseRow struct {
|
||||
ResponseCode *int32
|
||||
ResponseBody []byte
|
||||
RequestHash string
|
||||
}
|
||||
|
||||
func (q *Queries) GetIdempotentResponse(ctx context.Context, arg GetIdempotentResponseParams) (GetIdempotentResponseRow, error) {
|
||||
row := q.db.QueryRow(ctx, getIdempotentResponse, arg.Actor, arg.Key)
|
||||
var i GetIdempotentResponseRow
|
||||
err := row.Scan(&i.ResponseCode, &i.ResponseBody, &i.RequestHash)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const insertAuditEntry = `-- name: InsertAuditEntry :exec
|
||||
INSERT INTO audit_log (actor_type, actor_id, action, entity_id, method, path,
|
||||
status_code, detail, source_ip, correlation_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
`
|
||||
|
||||
type InsertAuditEntryParams struct {
|
||||
ActorType string
|
||||
ActorID *uuid.UUID
|
||||
Action string
|
||||
EntityID *uuid.UUID
|
||||
Method *string
|
||||
Path *string
|
||||
StatusCode *int32
|
||||
Detail []byte
|
||||
SourceIp *string
|
||||
CorrelationID *string
|
||||
}
|
||||
|
||||
func (q *Queries) InsertAuditEntry(ctx context.Context, arg InsertAuditEntryParams) error {
|
||||
_, err := q.db.Exec(ctx, insertAuditEntry,
|
||||
arg.ActorType,
|
||||
arg.ActorID,
|
||||
arg.Action,
|
||||
arg.EntityID,
|
||||
arg.Method,
|
||||
arg.Path,
|
||||
arg.StatusCode,
|
||||
arg.Detail,
|
||||
arg.SourceIp,
|
||||
arg.CorrelationID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const insertEvent = `-- name: InsertEvent :one
|
||||
INSERT INTO events (type, entity_id, severity, source, data, correlation_id)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id, ts
|
||||
`
|
||||
|
||||
type InsertEventParams struct {
|
||||
Type string
|
||||
EntityID *uuid.UUID
|
||||
Severity string
|
||||
Source string
|
||||
Data []byte
|
||||
CorrelationID *string
|
||||
}
|
||||
|
||||
type InsertEventRow struct {
|
||||
ID int64
|
||||
Ts time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) (InsertEventRow, error) {
|
||||
row := q.db.QueryRow(ctx, insertEvent,
|
||||
arg.Type,
|
||||
arg.EntityID,
|
||||
arg.Severity,
|
||||
arg.Source,
|
||||
arg.Data,
|
||||
arg.CorrelationID,
|
||||
)
|
||||
var i InsertEventRow
|
||||
err := row.Scan(&i.ID, &i.Ts)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listEntityStatus = `-- name: ListEntityStatus :many
|
||||
SELECT e.slug, e.type, st.health, st.last_check_at
|
||||
FROM entity_status st JOIN entities e ON e.id = st.entity_id
|
||||
ORDER BY e.slug
|
||||
`
|
||||
|
||||
type ListEntityStatusRow struct {
|
||||
Slug string
|
||||
Type string
|
||||
Health string
|
||||
LastCheckAt *time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) ListEntityStatus(ctx context.Context) ([]ListEntityStatusRow, error) {
|
||||
rows, err := q.db.Query(ctx, listEntityStatus)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListEntityStatusRow
|
||||
for rows.Next() {
|
||||
var i ListEntityStatusRow
|
||||
if err := rows.Scan(
|
||||
&i.Slug,
|
||||
&i.Type,
|
||||
&i.Health,
|
||||
&i.LastCheckAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listEvents = `-- name: ListEvents :many
|
||||
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
|
||||
FROM events
|
||||
WHERE ($1::text IS NULL OR type = $1)
|
||||
AND ($2::uuid IS NULL OR entity_id = $2)
|
||||
AND ($3::text IS NULL OR severity = $3)
|
||||
AND ($4::text IS NULL OR correlation_id = $4)
|
||||
AND ($5::timestamptz IS NULL OR ts >= $5)
|
||||
AND ($6::timestamptz IS NULL OR ts <= $6)
|
||||
AND ($7::bigint IS NULL OR id < $7)
|
||||
ORDER BY id DESC
|
||||
LIMIT $8
|
||||
`
|
||||
|
||||
type ListEventsParams struct {
|
||||
Type *string
|
||||
EntityID *uuid.UUID
|
||||
Severity *string
|
||||
CorrelationID *string
|
||||
FromTs *time.Time
|
||||
ToTs *time.Time
|
||||
BeforeID *int64
|
||||
Lim int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListEvents(ctx context.Context, arg ListEventsParams) ([]Event, error) {
|
||||
rows, err := q.db.Query(ctx, listEvents,
|
||||
arg.Type,
|
||||
arg.EntityID,
|
||||
arg.Severity,
|
||||
arg.CorrelationID,
|
||||
arg.FromTs,
|
||||
arg.ToTs,
|
||||
arg.BeforeID,
|
||||
arg.Lim,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Event
|
||||
for rows.Next() {
|
||||
var i Event
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Ts,
|
||||
&i.Type,
|
||||
&i.EntityID,
|
||||
&i.Severity,
|
||||
&i.Source,
|
||||
&i.Data,
|
||||
&i.CorrelationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listEventsAfter = `-- name: ListEventsAfter :many
|
||||
SELECT id, ts, type, entity_id, severity, source, data, correlation_id
|
||||
FROM events WHERE id > $1 ORDER BY id ASC LIMIT $2
|
||||
`
|
||||
|
||||
type ListEventsAfterParams struct {
|
||||
ID int64
|
||||
Limit int32
|
||||
}
|
||||
|
||||
func (q *Queries) ListEventsAfter(ctx context.Context, arg ListEventsAfterParams) ([]Event, error) {
|
||||
rows, err := q.db.Query(ctx, listEventsAfter, arg.ID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Event
|
||||
for rows.Next() {
|
||||
var i Event
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Ts,
|
||||
&i.Type,
|
||||
&i.EntityID,
|
||||
&i.Severity,
|
||||
&i.Source,
|
||||
&i.Data,
|
||||
&i.CorrelationID,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listSignals = `-- name: ListSignals :many
|
||||
SELECT sig.entity_id, se.slug, sig.kind, sig.severity, sig.state,
|
||||
te.slug AS target_slug, sig.check_id, sig.evidence, sig.likely_cause,
|
||||
sig.occurrence_count, sig.flap_count, sig.hold_down_until,
|
||||
sig.mute_until, sig.first_seen_at, sig.last_seen_at
|
||||
FROM signals sig
|
||||
JOIN entities se ON se.id = sig.entity_id
|
||||
LEFT JOIN entities te ON te.id = sig.target_entity_id
|
||||
WHERE ($1::text IS NULL OR sig.state = $1)
|
||||
AND ($2::text IS NULL OR sig.severity = $2)
|
||||
AND ($3::text IS NULL OR te.slug = $3)
|
||||
AND ($4::text IS NULL OR sig.kind = $4)
|
||||
AND ($5::text IS NULL OR se.slug > $5)
|
||||
ORDER BY se.slug
|
||||
LIMIT $6
|
||||
`
|
||||
|
||||
type ListSignalsParams struct {
|
||||
State *string
|
||||
Severity *string
|
||||
Target *string
|
||||
Kind *string
|
||||
Cursor *string
|
||||
Lim int32
|
||||
}
|
||||
|
||||
type ListSignalsRow struct {
|
||||
EntityID uuid.UUID
|
||||
Slug string
|
||||
Kind string
|
||||
Severity string
|
||||
State string
|
||||
TargetSlug *string
|
||||
CheckID *uuid.UUID
|
||||
Evidence *string
|
||||
LikelyCause *string
|
||||
OccurrenceCount int32
|
||||
FlapCount int32
|
||||
HoldDownUntil *time.Time
|
||||
MuteUntil *time.Time
|
||||
FirstSeenAt time.Time
|
||||
LastSeenAt time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) ListSignals(ctx context.Context, arg ListSignalsParams) ([]ListSignalsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listSignals,
|
||||
arg.State,
|
||||
arg.Severity,
|
||||
arg.Target,
|
||||
arg.Kind,
|
||||
arg.Cursor,
|
||||
arg.Lim,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListSignalsRow
|
||||
for rows.Next() {
|
||||
var i ListSignalsRow
|
||||
if err := rows.Scan(
|
||||
&i.EntityID,
|
||||
&i.Slug,
|
||||
&i.Kind,
|
||||
&i.Severity,
|
||||
&i.State,
|
||||
&i.TargetSlug,
|
||||
&i.CheckID,
|
||||
&i.Evidence,
|
||||
&i.LikelyCause,
|
||||
&i.OccurrenceCount,
|
||||
&i.FlapCount,
|
||||
&i.HoldDownUntil,
|
||||
&i.MuteUntil,
|
||||
&i.FirstSeenAt,
|
||||
&i.LastSeenAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const putIdempotentResponse = `-- name: PutIdempotentResponse :exec
|
||||
INSERT INTO idempotency_keys (actor, key, request_hash, response_code, response_body)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (actor, key) DO NOTHING
|
||||
`
|
||||
|
||||
type PutIdempotentResponseParams struct {
|
||||
Actor string
|
||||
Key string
|
||||
RequestHash string
|
||||
ResponseCode *int32
|
||||
ResponseBody []byte
|
||||
}
|
||||
|
||||
func (q *Queries) PutIdempotentResponse(ctx context.Context, arg PutIdempotentResponseParams) error {
|
||||
_, err := q.db.Exec(ctx, putIdempotentResponse,
|
||||
arg.Actor,
|
||||
arg.Key,
|
||||
arg.RequestHash,
|
||||
arg.ResponseCode,
|
||||
arg.ResponseBody,
|
||||
)
|
||||
return err
|
||||
}
|
||||
165
internal/db/sqlcgen/relationships.sql.go
Normal file
165
internal/db/sqlcgen/relationships.sql.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.29.0
|
||||
// source: relationships.sql
|
||||
|
||||
package sqlcgen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
const endCurrentRelationship = `-- name: EndCurrentRelationship :execrows
|
||||
UPDATE relationships SET valid_to = now()
|
||||
WHERE source_id = $1 AND target_id = $2 AND type = $3 AND valid_to IS NULL
|
||||
`
|
||||
|
||||
type EndCurrentRelationshipParams struct {
|
||||
SourceID uuid.UUID
|
||||
TargetID uuid.UUID
|
||||
Type string
|
||||
}
|
||||
|
||||
func (q *Queries) EndCurrentRelationship(ctx context.Context, arg EndCurrentRelationshipParams) (int64, error) {
|
||||
result, err := q.db.Exec(ctx, endCurrentRelationship, arg.SourceID, arg.TargetID, arg.Type)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return result.RowsAffected(), nil
|
||||
}
|
||||
|
||||
const listEntityRelations = `-- name: ListEntityRelations :many
|
||||
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
|
||||
r.valid_from, r.valid_to
|
||||
FROM relationships r
|
||||
JOIN entities se ON se.id = r.source_id
|
||||
JOIN entities te ON te.id = r.target_id
|
||||
WHERE r.valid_to IS NULL
|
||||
AND (($1::text IN ('out','both') AND r.source_id = $2)
|
||||
OR ($1::text IN ('in','both') AND r.target_id = $2))
|
||||
AND ($3::text IS NULL OR r.type = $3)
|
||||
ORDER BY r.type, se.slug, te.slug
|
||||
`
|
||||
|
||||
type ListEntityRelationsParams struct {
|
||||
Direction string
|
||||
ID uuid.UUID
|
||||
RelType *string
|
||||
}
|
||||
|
||||
type ListEntityRelationsRow struct {
|
||||
SourceSlug string
|
||||
TargetSlug string
|
||||
Type string
|
||||
Attributes []byte
|
||||
ValidFrom time.Time
|
||||
ValidTo *time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) ListEntityRelations(ctx context.Context, arg ListEntityRelationsParams) ([]ListEntityRelationsRow, error) {
|
||||
rows, err := q.db.Query(ctx, listEntityRelations, arg.Direction, arg.ID, arg.RelType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListEntityRelationsRow
|
||||
for rows.Next() {
|
||||
var i ListEntityRelationsRow
|
||||
if err := rows.Scan(
|
||||
&i.SourceSlug,
|
||||
&i.TargetSlug,
|
||||
&i.Type,
|
||||
&i.Attributes,
|
||||
&i.ValidFrom,
|
||||
&i.ValidTo,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listGraphEdges = `-- name: ListGraphEdges :many
|
||||
SELECT se.slug AS source_slug, te.slug AS target_slug, r.type, r.attributes,
|
||||
r.valid_from, r.valid_to
|
||||
FROM relationships r
|
||||
JOIN entities se ON se.id = r.source_id
|
||||
JOIN entities te ON te.id = r.target_id
|
||||
WHERE r.valid_to IS NULL
|
||||
AND r.source_id = ANY($1::uuid[])
|
||||
AND r.target_id = ANY($1::uuid[])
|
||||
AND ($2::text[] IS NULL OR r.type = ANY($2::text[]))
|
||||
ORDER BY r.type, se.slug, te.slug
|
||||
`
|
||||
|
||||
type ListGraphEdgesParams struct {
|
||||
Ids []uuid.UUID
|
||||
RelTypes []string
|
||||
}
|
||||
|
||||
type ListGraphEdgesRow struct {
|
||||
SourceSlug string
|
||||
TargetSlug string
|
||||
Type string
|
||||
Attributes []byte
|
||||
ValidFrom time.Time
|
||||
ValidTo *time.Time
|
||||
}
|
||||
|
||||
func (q *Queries) ListGraphEdges(ctx context.Context, arg ListGraphEdgesParams) ([]ListGraphEdgesRow, error) {
|
||||
rows, err := q.db.Query(ctx, listGraphEdges, arg.Ids, arg.RelTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListGraphEdgesRow
|
||||
for rows.Next() {
|
||||
var i ListGraphEdgesRow
|
||||
if err := rows.Scan(
|
||||
&i.SourceSlug,
|
||||
&i.TargetSlug,
|
||||
&i.Type,
|
||||
&i.Attributes,
|
||||
&i.ValidFrom,
|
||||
&i.ValidTo,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const upsertCurrentRelationship = `-- name: UpsertCurrentRelationship :exec
|
||||
INSERT INTO relationships (source_id, target_id, type, attributes, valid_from, valid_to)
|
||||
VALUES ($1, $2, $3, $4, now(), NULL)
|
||||
ON CONFLICT (source_id, target_id, type) WHERE valid_to IS NULL
|
||||
DO UPDATE SET attributes = EXCLUDED.attributes
|
||||
`
|
||||
|
||||
type UpsertCurrentRelationshipParams struct {
|
||||
SourceID uuid.UUID
|
||||
TargetID uuid.UUID
|
||||
Type string
|
||||
Attributes []byte
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertCurrentRelationship(ctx context.Context, arg UpsertCurrentRelationshipParams) error {
|
||||
_, err := q.db.Exec(ctx, upsertCurrentRelationship,
|
||||
arg.SourceID,
|
||||
arg.TargetID,
|
||||
arg.Type,
|
||||
arg.Attributes,
|
||||
)
|
||||
return err
|
||||
}
|
||||
65
internal/observability/record.go
Normal file
65
internal/observability/record.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package observability
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/dtoro/oikos/internal/db/sqlcgen"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Audit writes an audit_log entry. Pass a transaction-bound Queries so the
|
||||
// entry commits or rolls back atomically with the state change it records.
|
||||
//
|
||||
// actorLabel is the interim textual actor identity ("operator:dev",
|
||||
// "agent:mcp") recorded in detail; actor_id (a Person/Agent entity UUID)
|
||||
// starts being populated when OIDC identity resolution lands.
|
||||
func Audit(ctx context.Context, q *sqlcgen.Queries, actorType, actorLabel,
|
||||
action string, entityID *uuid.UUID, method, path, correlationID string,
|
||||
detail map[string]any) error {
|
||||
|
||||
if detail == nil {
|
||||
detail = map[string]any{}
|
||||
}
|
||||
detail["actor"] = actorLabel
|
||||
detailJSON, _ := json.Marshal(detail)
|
||||
|
||||
var corr *string
|
||||
if correlationID != "" {
|
||||
corr = &correlationID
|
||||
}
|
||||
return q.InsertAuditEntry(ctx, sqlcgen.InsertAuditEntryParams{
|
||||
ActorType: actorType,
|
||||
Action: action,
|
||||
EntityID: entityID,
|
||||
Method: &method,
|
||||
Path: &path,
|
||||
Detail: detailJSON,
|
||||
CorrelationID: corr,
|
||||
})
|
||||
}
|
||||
|
||||
// Event emits a structured event in the caller's transaction (SG10). The
|
||||
// post-commit NOTIFY trigger (migration 008) fans it out to SSE subscribers.
|
||||
func Event(ctx context.Context, q *sqlcgen.Queries, eventType string,
|
||||
entityID *uuid.UUID, severity, source string, correlationID string,
|
||||
data map[string]any) error {
|
||||
|
||||
dataJSON, _ := json.Marshal(data)
|
||||
if data == nil {
|
||||
dataJSON = []byte("{}")
|
||||
}
|
||||
var corr *string
|
||||
if correlationID != "" {
|
||||
corr = &correlationID
|
||||
}
|
||||
_, err := q.InsertEvent(ctx, sqlcgen.InsertEventParams{
|
||||
Type: eventType,
|
||||
EntityID: entityID,
|
||||
Severity: severity,
|
||||
Source: source,
|
||||
Data: dataJSON,
|
||||
CorrelationID: corr,
|
||||
})
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user