Merge: Slice B.5 — Go type aliases (SequencingRule = DeadlineRule) + JSON envelope dual-emit + Deprecation headers (m/paliad#93)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"mgit.msbls.de/m/paliad/internal/models"
|
||||
"mgit.msbls.de/m/paliad/internal/services"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,60 @@ import (
|
||||
// is mapped to 409 Conflict so the editor UI can show a clear "must
|
||||
// clone first" hint.
|
||||
|
||||
// Slice B.5 (t-paliad-305) JSON envelope renames:
|
||||
//
|
||||
// - submission_code → code (procedural-event identifier)
|
||||
// - event_type → event_kind (procedural-event taxonomy)
|
||||
//
|
||||
// Wire compatibility: every response emits BOTH the legacy and the
|
||||
// canonical keys for one slice (see Deprecation HTTP header on the
|
||||
// response). Input bodies accept either name on the request; the
|
||||
// canonical key wins when both are present.
|
||||
//
|
||||
// adminRuleResponse wraps models.DeadlineRule (= litigationplanner.Rule)
|
||||
// to add the canonical `code` + `event_kind` fields alongside the
|
||||
// historical `submission_code` + `event_type` already on Rule's tags.
|
||||
// The embedded *models.DeadlineRule carries every existing tag through
|
||||
// json.Marshal unchanged; the wrapper only ADDS the two new keys.
|
||||
type adminRuleResponse struct {
|
||||
*models.DeadlineRule
|
||||
Code *string `json:"code,omitempty"`
|
||||
EventKind *string `json:"event_kind,omitempty"`
|
||||
}
|
||||
|
||||
// wrapRuleResponse builds the dual-emit wrapper from a service result.
|
||||
// Same values, two keys per concept — no semantic change.
|
||||
func wrapRuleResponse(r *models.DeadlineRule) adminRuleResponse {
|
||||
if r == nil {
|
||||
return adminRuleResponse{}
|
||||
}
|
||||
return adminRuleResponse{
|
||||
DeadlineRule: r,
|
||||
Code: r.SubmissionCode,
|
||||
EventKind: r.EventType,
|
||||
}
|
||||
}
|
||||
|
||||
// wrapRuleListResponse maps a slice of service results into the
|
||||
// dual-emit wrapper. Used by the LIST endpoint.
|
||||
func wrapRuleListResponse(rows []models.DeadlineRule) []adminRuleResponse {
|
||||
out := make([]adminRuleResponse, len(rows))
|
||||
for i := range rows {
|
||||
out[i] = wrapRuleResponse(&rows[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// adminRuleDeprecationHeaders writes the IETF "Deprecation" + "Sunset"
|
||||
// HTTP headers signaling that the legacy `submission_code` /
|
||||
// `event_type` JSON keys are being retired in favour of `code` /
|
||||
// `event_kind`. RFC 8594 (Sunset) + draft-ietf-httpapi-deprecation-header.
|
||||
// Clients should migrate within one slice cycle.
|
||||
func adminRuleDeprecationHeaders(w http.ResponseWriter) {
|
||||
w.Header().Set("Deprecation", `true; key="submission_code,event_type"`)
|
||||
w.Header().Set("Link", `<https://mgit.msbls.de/m/paliad/issues/93>; rel="deprecation"`)
|
||||
}
|
||||
|
||||
// GET /admin/api/rules — paginated list with filters.
|
||||
func handleAdminListRules(w http.ResponseWriter, r *http.Request) {
|
||||
if dbSvc == nil || dbSvc.ruleEditor == nil {
|
||||
@@ -73,7 +128,8 @@ func handleAdminListRules(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, rows)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleListResponse(rows))
|
||||
}
|
||||
|
||||
// GET /admin/api/rules/{id}
|
||||
@@ -91,7 +147,8 @@ func handleAdminGetRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// POST /admin/api/rules — create draft.
|
||||
@@ -108,12 +165,15 @@ func handleAdminCreateRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
return
|
||||
}
|
||||
// Slice B.5 (t-paliad-305): accept both legacy + canonical JSON keys.
|
||||
body.CreateRuleInput.CoalesceCanonicalKeys()
|
||||
row, err := dbSvc.ruleEditor.Create(r.Context(), body.CreateRuleInput, body.Reason)
|
||||
if err != nil {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusCreated, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// PATCH /admin/api/rules/{id} — partial update of a draft.
|
||||
@@ -134,12 +194,15 @@ func handleAdminPatchRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
|
||||
return
|
||||
}
|
||||
// Slice B.5 (t-paliad-305): accept both legacy + canonical JSON keys.
|
||||
body.RulePatch.CoalesceCanonicalKeys()
|
||||
row, err := dbSvc.ruleEditor.UpdateDraft(r.Context(), id, body.RulePatch, body.Reason)
|
||||
if err != nil {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// POST /admin/api/rules/{id}/clone-as-draft
|
||||
@@ -161,7 +224,8 @@ func handleAdminCloneAsDraft(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusCreated, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// POST /admin/api/rules/{id}/publish
|
||||
@@ -183,7 +247,8 @@ func handleAdminPublishRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// POST /admin/api/rules/{id}/archive
|
||||
@@ -205,7 +270,8 @@ func handleAdminArchiveRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// POST /admin/api/rules/{id}/restore
|
||||
@@ -227,7 +293,8 @@ func handleAdminRestoreRule(w http.ResponseWriter, r *http.Request) {
|
||||
writeRuleEditorError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, row)
|
||||
adminRuleDeprecationHeaders(w)
|
||||
writeJSON(w, http.StatusOK, wrapRuleResponse(row))
|
||||
}
|
||||
|
||||
// GET /admin/api/rules/{id}/audit?offset=N&limit=M
|
||||
|
||||
@@ -553,6 +553,51 @@ type Party struct {
|
||||
// scans, hydration, projection service) continues to compile.
|
||||
type DeadlineRule = litigationplanner.Rule
|
||||
|
||||
// SequencingRule is the Slice B.5 (t-paliad-305) canonical name for what
|
||||
// the legacy schema called a "deadline rule". Alias to DeadlineRule so
|
||||
// existing call-sites compile unchanged while new code can adopt the
|
||||
// procedural-event vocabulary. Same struct, same db / json tags.
|
||||
type SequencingRule = DeadlineRule
|
||||
|
||||
// ProceduralEvent mirrors paliad.procedural_events — the "what kind of
|
||||
// step is this in the proceeding" identity row. New struct introduced
|
||||
// in Slice B.5 (t-paliad-305) for code that needs the procedural-event
|
||||
// columns alone. Most consumers still pull the merged shape via
|
||||
// SequencingRule through the paliad.deadline_rules_unified view; this
|
||||
// struct unlocks per-PE reads/writes without going through the view.
|
||||
type ProceduralEvent struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Code string `db:"code" json:"code"`
|
||||
Name string `db:"name" json:"name"`
|
||||
NameEN string `db:"name_en" json:"name_en"`
|
||||
Description *string `db:"description" json:"description,omitempty"`
|
||||
EventKind *string `db:"event_kind" json:"event_kind,omitempty"`
|
||||
PrimaryPartyDefault *string `db:"primary_party_default" json:"primary_party_default,omitempty"`
|
||||
LegalSourceID *uuid.UUID `db:"legal_source_id" json:"legal_source_id,omitempty"`
|
||||
ConceptID *uuid.UUID `db:"concept_id" json:"concept_id,omitempty"`
|
||||
LifecycleState string `db:"lifecycle_state" json:"lifecycle_state"`
|
||||
DraftOf *uuid.UUID `db:"draft_of" json:"draft_of,omitempty"`
|
||||
PublishedAt *time.Time `db:"published_at" json:"published_at,omitempty"`
|
||||
IsActive bool `db:"is_active" json:"is_active"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// LegalSource mirrors paliad.legal_sources — the source-of-law citation
|
||||
// rows that procedural events anchor against. pretty_de / pretty_en are
|
||||
// nullable on disk; readers fall back to
|
||||
// internal/services/submission_vars.go:legalSourcePretty when missing.
|
||||
type LegalSource struct {
|
||||
ID uuid.UUID `db:"id" json:"id"`
|
||||
Citation string `db:"citation" json:"citation"`
|
||||
Jurisdiction string `db:"jurisdiction" json:"jurisdiction"`
|
||||
PrettyDE *string `db:"pretty_de" json:"pretty_de,omitempty"`
|
||||
PrettyEN *string `db:"pretty_en" json:"pretty_en,omitempty"`
|
||||
Notes *string `db:"notes" json:"notes,omitempty"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// DeadlineRuleAudit is one row of paliad.deadline_rule_audit — the
|
||||
// append-only audit log for every change to paliad.deadline_rules.
|
||||
// Written by the AFTER-trigger (raw create / update / delete) and by
|
||||
|
||||
@@ -10,12 +10,24 @@ import (
|
||||
"mgit.msbls.de/m/paliad/internal/models"
|
||||
)
|
||||
|
||||
// DeadlineRuleService reads paliad.deadline_rules + paliad.proceeding_types.
|
||||
// Rules are static reference data; no visibility check needed.
|
||||
// DeadlineRuleService reads paliad.deadline_rules_unified (mig 139 view
|
||||
// projecting paliad.sequencing_rules + procedural_events +
|
||||
// legal_sources back to the legacy column shape after mig 140 dropped
|
||||
// the underlying table) + paliad.proceeding_types. Rules are static
|
||||
// reference data; no visibility check needed.
|
||||
type DeadlineRuleService struct {
|
||||
db *sqlx.DB
|
||||
}
|
||||
|
||||
// SequencingRuleService is the Slice B.5 (t-paliad-305) canonical name
|
||||
// for DeadlineRuleService. Alias preserves every existing call-site
|
||||
// while new code can adopt the procedural-event vocabulary.
|
||||
type SequencingRuleService = DeadlineRuleService
|
||||
|
||||
// NewSequencingRuleService is the canonical constructor name; alias to
|
||||
// NewDeadlineRuleService for now. Both return the same underlying type.
|
||||
var NewSequencingRuleService = NewDeadlineRuleService
|
||||
|
||||
// NewDeadlineRuleService wires the service to the pool.
|
||||
func NewDeadlineRuleService(db *sqlx.DB) *DeadlineRuleService {
|
||||
return &DeadlineRuleService{db: db}
|
||||
|
||||
@@ -76,7 +76,13 @@ type RulePatch struct {
|
||||
NameEN *string `json:"name_en,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
PrimaryParty *string `json:"primary_party,omitempty"`
|
||||
// EventType is the legacy JSON key; EventKind is the Slice B.5
|
||||
// canonical name. Decoder accepts either — coalescePatchKeys()
|
||||
// resolves the canonical to the legacy field if only EventKind
|
||||
// was sent. Same uuid wire shape; emit-side wraps via
|
||||
// adminRuleResponse to expose both keys for one slice.
|
||||
EventType *string `json:"event_type,omitempty"`
|
||||
EventKind *string `json:"event_kind,omitempty"`
|
||||
DurationValue *int `json:"duration_value,omitempty"`
|
||||
DurationUnit *string `json:"duration_unit,omitempty"`
|
||||
Timing *string `json:"timing,omitempty"`
|
||||
@@ -101,6 +107,24 @@ type RulePatch struct {
|
||||
ConceptID *uuid.UUID `json:"concept_id,omitempty"`
|
||||
}
|
||||
|
||||
// CoalesceCanonicalKeys folds the Slice B.5 (t-paliad-305) canonical
|
||||
// JSON aliases into the legacy field positions so the rest of the
|
||||
// service can keep using the existing field names. Canonical wins
|
||||
// when both are sent.
|
||||
//
|
||||
// json:"event_kind" → EventType (legacy)
|
||||
//
|
||||
// Called by the handler immediately after json.Decode. New code can
|
||||
// adopt the canonical naming; legacy callers continue to work.
|
||||
func (p *RulePatch) CoalesceCanonicalKeys() {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if p.EventKind != nil {
|
||||
p.EventType = p.EventKind
|
||||
}
|
||||
}
|
||||
|
||||
// CreateRuleInput is the create payload — a full rule row in draft
|
||||
// state. Required fields enforce schema NOT-NULL on insert (name,
|
||||
// name_en, duration_value, duration_unit).
|
||||
@@ -111,9 +135,16 @@ type CreateRuleInput struct {
|
||||
TriggerEventID *int64 `json:"trigger_event_id,omitempty"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
ConceptID *uuid.UUID `json:"concept_id,omitempty"`
|
||||
// SubmissionCode is the legacy JSON key; Code is the Slice B.5
|
||||
// canonical name. Decoder accepts either — CoalesceCanonicalKeys()
|
||||
// folds Code → SubmissionCode if only the canonical was sent.
|
||||
SubmissionCode *string `json:"submission_code,omitempty"`
|
||||
Code *string `json:"code,omitempty"`
|
||||
PrimaryParty *string `json:"primary_party,omitempty"`
|
||||
// EventType is the legacy JSON key; EventKind is the Slice B.5
|
||||
// canonical name. Same dual-accept pattern as SubmissionCode/Code.
|
||||
EventType *string `json:"event_type,omitempty"`
|
||||
EventKind *string `json:"event_kind,omitempty"`
|
||||
DurationValue int `json:"duration_value"`
|
||||
DurationUnit string `json:"duration_unit"`
|
||||
Timing *string `json:"timing,omitempty"`
|
||||
@@ -135,6 +166,24 @@ type CreateRuleInput struct {
|
||||
SequenceOrder int `json:"sequence_order"`
|
||||
}
|
||||
|
||||
// CoalesceCanonicalKeys folds the Slice B.5 (t-paliad-305) canonical
|
||||
// JSON aliases into the legacy field positions. Canonical wins when
|
||||
// both are sent. Called by the handler immediately after json.Decode.
|
||||
//
|
||||
// json:"code" → SubmissionCode (legacy)
|
||||
// json:"event_kind" → EventType (legacy)
|
||||
func (in *CreateRuleInput) CoalesceCanonicalKeys() {
|
||||
if in == nil {
|
||||
return
|
||||
}
|
||||
if in.Code != nil {
|
||||
in.SubmissionCode = in.Code
|
||||
}
|
||||
if in.EventKind != nil {
|
||||
in.EventType = in.EventKind
|
||||
}
|
||||
}
|
||||
|
||||
// Create inserts a new rule as lifecycle_state='draft' with
|
||||
// published_at=NULL. The caller's reason is set on the session BEFORE
|
||||
// the INSERT so the mig 079 trigger writes an audit row with the
|
||||
|
||||
Reference in New Issue
Block a user