Files
shankar0123 21aeed4f4e legal: addlicense headers + normalize legacy variants (Phase 0 RED-4)
Phase 0 closure (Path B2, post-rewrite):

addlicense sweep — adds the canonical certctl LLC copyright + BUSL-1.1
SPDX header to every production Go file. Template:

  // Copyright 2026 certctl LLC. All rights reserved.
  // SPDX-License-Identifier: BUSL-1.1

Coverage: 338 / 338 production Go files (cmd/ + internal/, excluding
*_test.go and **/testdata/**). Pre-sweep coverage was 22 / 338 (6.5%);
post-sweep is 338 / 338 (100%).

Normalized 22 pre-existing legacy headers (`// Copyright (c) certctl`
+ `// SPDX-License-Identifier: BSL-1.1`) and 1 file using a
`Certctl Contributors` attribution. The legacy SPDX ID `BSL-1.1`
is non-standard; the official SPDX identifier for Business Source
License 1.1 is `BUSL-1.1` (capital U). All 338 files now share the
canonical form.

Generated via:
  addlicense -c "certctl LLC" -y 2026 \
    -f cowork/legal/copyright-header.tpl \
    -ignore '**/testdata/**' -ignore '**/*_test.go' \
    cmd/ internal/

Verification:
  find cmd internal -name '*.go' -not -name '*_test.go' \
    -not -path '*/testdata/*' \
    -exec grep -L '^// Copyright 2026 certctl LLC' {} \; | wc -l

  Returns: 0

gofmt clean. Header additions are comments only, no compile impact.

Closes: cowork/certctl-architecture-diligence-audit.html#fix-RED-4
2026-05-13 21:23:35 +00:00

105 lines
3.7 KiB
Go

// Copyright 2026 certctl LLC. All rights reserved.
// SPDX-License-Identifier: BUSL-1.1
// WithinTx is the transactional spine for any service-layer operation
// whose audit row must be atomic with the underlying state change.
// Closes the #3 acquisition-readiness blocker from the 2026-05-01
// issuer coverage audit (Part 1.5 finding #1: audit row not
// transactional with issuance).
//
// The Querier interface lives in internal/repository (shared with the
// interface declarations) so repository interfaces and the postgres
// concrete types reference the same type without a circular import.
package postgres
import (
"context"
"database/sql"
"fmt"
"github.com/certctl-io/certctl/internal/repository"
)
// transactor is the production implementation of repository.Transactor.
// It wraps a *sql.DB and exposes the WithinTx helper as the interface
// method service-layer code calls.
type transactor struct {
db *sql.DB
}
// NewTransactor returns a repository.Transactor backed by the given
// *sql.DB. Production wiring (cmd/server/main.go) passes the same db
// handle that backs the other repositories; tests pass a mock that
// implements the interface against in-memory state.
func NewTransactor(db *sql.DB) repository.Transactor {
return &transactor{db: db}
}
// WithinTx delegates to the package-level WithinTx helper, adapting
// the function signature so callers receive repository.Querier instead
// of *sql.Tx (which the interface requires for portability across
// transactor implementations).
func (t *transactor) WithinTx(ctx context.Context, fn func(q repository.Querier) error) error {
return WithinTx(ctx, t.db, func(tx *sql.Tx) error {
return fn(tx)
})
}
// Querier is re-exported from the parent repository package so callers
// inside this package can reference it without an extra import.
//
// Deprecated: external callers should use repository.Querier directly.
// This alias exists for legibility within the postgres package only.
// WithinTx runs fn inside a transaction. The transaction is committed
// if fn returns nil; rolled back if fn returns an error or panics.
//
// Contract:
//
// - On nil error from fn: tx.Commit() is called. If Commit fails
// (e.g., serialization conflict, connection drop), the commit
// error is returned.
// - On non-nil error from fn: tx.Rollback() is called. If Rollback
// itself errors, the original fn error is wrapped with the
// rollback error so operators see both.
// - On panic in fn: tx.Rollback() is called and the panic is
// re-raised. The transaction is never left dangling.
//
// Callers must NOT call tx.Commit() or tx.Rollback() inside fn — that's
// WithinTx's job. Returning an error from fn signals "roll back";
// returning nil signals "commit".
//
// BeginTx is called with nil opts; callers needing isolation level
// other than the database default should construct their own tx via
// db.BeginTx and not use this helper.
func WithinTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback()
// ARCH-L1: re-throw the recovered panic after rolling back
// the transaction. The Tx layer's contract is "preserve
// panics across the rollback boundary"; swallowing here
// would hide the original bug from the caller.
panic(p)
}
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil {
err = fmt.Errorf("%w; rollback: %v", err, rbErr)
}
}
}()
if err = fn(tx); err != nil {
return err
}
if cmErr := tx.Commit(); cmErr != nil {
return fmt.Errorf("commit tx: %w", cmErr)
}
return nil
}