mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:41:41 +00:00
21aeed4f4e
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
105 lines
3.7 KiB
Go
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
|
|
}
|