m/paliad#61 Slice A. Introduces paliad.checklists (mig 114) as the DB-backed companion to the static Go catalog. ChecklistCatalogService unifies both sources at read time; ChecklistTemplateService handles authoring CRUD + visibility toggle (private↔firm; Slice B opens 'shared' and 'global'). Schema (mig 114, idempotent): - paliad.checklists (uuid, slug UNIQUE, owner_id FK, title/description /regime/court/reference/deadline/lang, body jsonb, visibility CHECK ('private','shared','firm','global'), promoted_at/_by, timestamps) - paliad.can_see_checklist(uuid, uuid) STABLE SECURITY DEFINER — owner OR firm/global. Slice B extends with the explicit-share branch. - RLS: select via can_see_checklist; insert owner=self; update/delete owner OR global_admin - ALTER paliad.checklist_instances ADD COLUMN template_snapshot jsonb (snapshot semantics so per-Akte instances stay decoupled from subsequent template edits) Services: - ChecklistCatalogService — ListVisible, Find, SnapshotBody, IsStaticSlug. Reapplies visibility application-side (service-role bypasses RLS, per visibility.go pattern). Static-slug map computed once at boot for collision detection. - ChecklistTemplateService — Create (auto-generates u-<slug>-<hex> with retry), Update (changed_fields[] in audit), SetVisibility, Delete, ListOwnedBy, GetBySlug. Owner-or-global_admin gate. - SystemAuditLogService.WriteChecklistEvent — thin helper writing into paliad.system_audit_log with scope='org'. - ChecklistInstanceService.Create now captures template_snapshot via the catalog; GetByID returns it inline so the frontend can render the captured body even after the upstream template is mutated. Endpoints (all owner-gated where mutating): - GET /api/checklists — merged catalog (static + DB visible) - GET /api/checklists/{slug} — single template; static-first lookup - GET /api/checklists/templates/mine — caller's authored templates - POST /api/checklists/templates — create - PATCH /api/checklists/templates/{slug} — edit - PATCH /api/checklists/templates/{slug}/visibility — private↔firm - DELETE /api/checklists/templates/{slug} — delete - GET /checklists/new, /checklists/{slug}/edit — author wizard pages Tests: pure-helper unit tests cover slugifyTitle (umlaut → ae/oe/ue/ss normalisation + clamp), regime/lang/visibility validation, body-shape enforcement, static-slug detection, predicate shape, clamp.
69 lines
2.1 KiB
Go
69 lines
2.1 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// SystemAuditLogService is a thin write helper for paliad.system_audit_log
|
|
// (mig 102). Each domain emits its own event_type prefix
|
|
// (checklist.* / data_export* / …) so dashboards can group by feature.
|
|
//
|
|
// The audit row is best-effort INSIDE the caller's transaction — the
|
|
// caller passes its in-flight *sqlx.Tx so the audit write rolls back
|
|
// with the data change if anything else fails.
|
|
type SystemAuditLogService struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
func NewSystemAuditLogService(db *sqlx.DB) *SystemAuditLogService {
|
|
return &SystemAuditLogService{db: db}
|
|
}
|
|
|
|
// ChecklistAuditEvent is the input shape for the WriteChecklistEvent
|
|
// helper. Scope defaults to 'org' since template-level events are firm-
|
|
// wide; instance-level events stay on paliad.project_events via the
|
|
// existing helpers.
|
|
type ChecklistAuditEvent struct {
|
|
EventType string // e.g. "checklist.authored", "checklist.edited"
|
|
ActorID uuid.UUID
|
|
ActorEmail string // captured at write time; survives user deletion
|
|
Metadata map[string]any
|
|
}
|
|
|
|
// WriteChecklistEvent inserts a row into paliad.system_audit_log with
|
|
// scope='org' and scope_root=NULL. Metadata is JSON-encoded.
|
|
func (s *SystemAuditLogService) WriteChecklistEvent(ctx context.Context, tx *sqlx.Tx, evt ChecklistAuditEvent) error {
|
|
if evt.EventType == "" {
|
|
return fmt.Errorf("system_audit_log: event_type required")
|
|
}
|
|
if evt.Metadata == nil {
|
|
evt.Metadata = map[string]any{}
|
|
}
|
|
mb, err := json.Marshal(evt.Metadata)
|
|
if err != nil {
|
|
return fmt.Errorf("system_audit_log marshal: %w", err)
|
|
}
|
|
exec := func(q string, args ...any) error {
|
|
if tx != nil {
|
|
_, err := tx.ExecContext(ctx, q, args...)
|
|
return err
|
|
}
|
|
_, err := s.db.ExecContext(ctx, q, args...)
|
|
return err
|
|
}
|
|
if err := exec(
|
|
`INSERT INTO paliad.system_audit_log
|
|
(event_type, actor_id, actor_email, scope, scope_root, metadata)
|
|
VALUES ($1, $2, $3, 'org', NULL, $4::jsonb)`,
|
|
evt.EventType, evt.ActorID, evt.ActorEmail, string(mb),
|
|
); err != nil {
|
|
return fmt.Errorf("system_audit_log insert: %w", err)
|
|
}
|
|
return nil
|
|
}
|