mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-11 16:38:52 +00:00
G-1: renewal-policies API + frontend FK-drift fix
Three frontend call sites (OnboardingWizard.tsx:603, CertificatesPage.tsx:52,
CertificateDetailPage.tsx:169) populated the renewal_policy_id dropdown from
getPolicies() — the compliance-rule endpoint returning pol-* IDs — which
violated the FK managed_certificates.renewal_policy_id REFERENCES
renewal_policies(id) ON DELETE RESTRICT. Create would fail pg 23503 at insert.
Backend (new):
- RenewalPolicyRepository CRUD + ListAll/ExistsByID (pg 23503 → ErrRenewalPolicyInUse
→ HTTP 409; pg 23505 → ErrRenewalPolicyDuplicateName → HTTP 409)
- RenewalPolicyService with repo-only constructor. Service sentinels
var-alias the repo sentinels so errors.Is walks across layers.
- RenewalPolicyHandler with validation bounds: name 1–255;
renewal_window_days [1,365] default 30; max_retries [0,10] not defaulted;
retry_interval_seconds [60,86400] default 3600; alert_thresholds_days
[0,365] default [30,14,7,0]. Auto-generated IDs rp-<slug(name)>.
- Router registers 5 routes under /api/v1/renewal-policies[/{id}].
Frontend:
- CertificatesPage/CertificateDetailPage/OnboardingWizard now call
getRenewalPolicies() and render rp-* IDs.
- client.ts adds getRenewalPolicies/createRenewalPolicy/updateRenewalPolicy/
deleteRenewalPolicy. types.ts adds the RenewalPolicy shape.
OpenAPI: RenewalPolicies tag + 5 operations + 3 schemas (RenewalPolicy,
RenewalPolicyCreateRequest, RenewalPolicyUpdateRequest). 409 responses
on create/update duplicate-name and delete FK-in-use.
No migration — renewal_policies table already exists from the initial
schema (000001).
Tests:
- internal/service/renewal_policy_test.go: CRUD + validation + sentinel
error wrapping.
- internal/api/handler/renewal_policy_handler_test.go: handler endpoint
contracts including 400/404/409.
- web/src/api/client.test.ts: 4 subtests covering the 4 new API functions.
Phase 3 gates all green: go vet, build, short tests, race tests (service/
handler/router/scheduler), staticcheck (G-1 packages), govulncheck (0
reachable), coverage (service 69.7%, handler 79.0%, domain 86.9%,
middleware 80.6% — all above thresholds), tsc, vitest (256 passed),
vite build, OpenAPI structural validation.
This commit is contained in:
@@ -2,11 +2,27 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// Repository-level sentinel errors. Repositories (primarily the postgres
|
||||
// implementation) translate RDBMS-specific errors into these typed envelopes
|
||||
// so the service/handler layers can branch with errors.Is without importing
|
||||
// lib/pq or care about SQLSTATE codes.
|
||||
//
|
||||
// G-1: renewal-policy sentinels — DuplicateName → HTTP 409 (pg 23505 on
|
||||
// renewal_policies.name UNIQUE), InUse → HTTP 409 (pg 23503 on the FK from
|
||||
// managed_certificates.renewal_policy_id to renewal_policies.id with ON
|
||||
// DELETE RESTRICT). Both map onto the same 409 status but with distinct
|
||||
// messages so operators can tell them apart.
|
||||
var (
|
||||
ErrRenewalPolicyDuplicateName = errors.New("renewal policy name already exists")
|
||||
ErrRenewalPolicyInUse = errors.New("renewal policy is still referenced by managed certificates")
|
||||
)
|
||||
|
||||
// CertificateRepository defines operations for managing certificates.
|
||||
type CertificateRepository interface {
|
||||
// List returns a paginated list of certificates matching the filter criteria.
|
||||
@@ -258,11 +274,35 @@ type JobRepository interface {
|
||||
}
|
||||
|
||||
// RenewalPolicyRepository defines operations for managing renewal policies.
|
||||
//
|
||||
// G-1: extended with Create/Update/Delete so the new /api/v1/renewal-policies
|
||||
// CRUD surface has a repo contract to lean on. Delete must map the PostgreSQL
|
||||
// 23503 (foreign_key_violation on managed_certificates.renewal_policy_id
|
||||
// REFERENCES renewal_policies(id) ON DELETE RESTRICT) onto the typed
|
||||
// ErrRenewalPolicyInUse sentinel so the handler can emit a 409 Conflict
|
||||
// instead of an opaque 500. Create/Update map PostgreSQL 23505
|
||||
// (unique_violation on renewal_policies.name) onto ErrRenewalPolicyDuplicateName
|
||||
// for the same 409 Conflict reason.
|
||||
//
|
||||
// List stays single-shot (no pagination params) because the production row
|
||||
// count is in the single digits — the service layer paginates/sorts in Go.
|
||||
// Changing the signature would churn every mock without functional benefit.
|
||||
type RenewalPolicyRepository interface {
|
||||
// Get retrieves a renewal policy by ID.
|
||||
Get(ctx context.Context, id string) (*domain.RenewalPolicy, error)
|
||||
// List returns all renewal policies.
|
||||
// List returns all renewal policies, ordered by name.
|
||||
List(ctx context.Context) ([]*domain.RenewalPolicy, error)
|
||||
// Create inserts a new renewal policy. The caller is responsible for
|
||||
// populating Name; Create auto-generates ID (as rp-<slug(name)>) if empty.
|
||||
// Returns ErrRenewalPolicyDuplicateName on pg 23505.
|
||||
Create(ctx context.Context, policy *domain.RenewalPolicy) error
|
||||
// Update modifies an existing renewal policy in-place. Returns
|
||||
// sql.ErrNoRows-wrapped error when id is unknown, or
|
||||
// ErrRenewalPolicyDuplicateName on pg 23505 (name collision with another row).
|
||||
Update(ctx context.Context, id string, policy *domain.RenewalPolicy) error
|
||||
// Delete removes a renewal policy. Returns ErrRenewalPolicyInUse when the
|
||||
// policy is still referenced by rows in managed_certificates (pg 23503).
|
||||
Delete(ctx context.Context, id string) error
|
||||
}
|
||||
|
||||
// PolicyRepository defines operations for managing compliance policies and violations.
|
||||
|
||||
Reference in New Issue
Block a user