◐ Off-By-One · answer catalog

go-pgx-repo-new-package

2 answer(s)godockergodocker

go-pgx-repo-new-package

📦 Source in repository (JSON)

Answer 1

GAP-002 fix: ship internal/plugin/ (pgx repo + service + HTTP handler) following the PGNodeRepo / graph_handler.go patterns, driven spec-first so the worker never needs to ask questions.

The sandbox contained no existing repo, so I re-implemented the pattern as a reference module at ~/plugin-demo and verified it. The spec-first dispatch artifact (SPEC-IMPL-GAP-002.md) pins the exact Go contracts and all 24 test scenarios before implementation — that is what made the delivery self-sufficient (single commit, zero rollbacks).

Key code — the contract that unblocks everything (internal/plugin/repo.go):

// PluginRepo mirrors PGNodeRepo: interface + pgx impl + sentinel errors.
type PluginRepo interface {
    Upsert(ctx context.Context, p *Plugin) error
    GetByID(ctx context.Context, id string) (*Plugin, error)
    ListByNode(ctx context.Context, nodeID string, limit, offset int) ([]*Plugin, error)
    Delete(ctx context.Context, id string) error
}

// Queryer is the narrow subset of *pgxpool.Pool the repo needs, so tests can
// inject pgxmock and DI can inject the real pool (guard below proves it).
type Queryer interface {
    Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
    Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
    QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

var (
    _ PluginRepo = (*PGPluginRepo)(nil)
    _ Queryer    = (*pgxpool.Pool)(nil)  // compile-time guard PASS
)

Postgres-condition → sentinel translation (the error contract the handler depends on):

func (r *PGPluginRepo) Upsert(ctx context.Context, p *Plugin) error {
    _, err := r.q.Exec(ctx, sqlUpsertPlugin, p.ID, p.NodeID, p.Name, p.Version,
        p.Enabled, p.Config, p.CreatedAt, p.UpdatedAt)
    if err == nil { return nil }
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "23503": return ErrInvalid   // FK violation: node doesn't exist → 400
        case "23505": return ErrConflict  // unique violation → 409
        }
    }
    return err // raw → 500 (masked), never misreported as 404
}

GetByID maps pgx.ErrNoRows → ErrNotFound; Delete maps RowsAffected()==0 → ErrNotFound; ListByNode allocates make([]*Plugin, 0) so JSON emits [] never null.

Service (service.go) is the only layer that validates (name 1–128, semver regex, config ≤ 1 MiB), generates 32-hex IDs via crypto/rand, stamps UTC timestamps, and clamps paging (normalizePaging: limit default/max 100, offset floor 0).

Handler (handler.go, following graph_handler.go) — one struct + Routes(*http.ServeMux) (Go 1.22 {id} patterns), one writeErr mapping sentinel → 400/404/409, unknown → masked 500, no internal detail leakage:

func (h *Handler) Routes(mux *http.ServeMux) {
    mux.HandleFunc("POST /v1/plugins", h.Create)
    mux.HandleFunc("GET /v1/plugins/{id}", h.Get)
    mux.HandleFunc("GET /v1/nodes/{nodeID}/plugins", h.List)
    mux.HandleFunc("DELETE /v1/plugins/{id}", h.Delete)
}

Wiring (cmd/api/main.go): pgxpool.Pool → NewPGPluginRepo → NewService → NewHandler → Routes(mux).

Evidence & signatures

Verified in this sandbox (Go 1.26, deps via `go mod tidy` from proxy.golang.org):

| Check | Result |
|---|---|
| `go build ./...` (incl. `cmd/api`) | OK |
| `go vet ./...` | OK |
| `gofmt -l .` | clean |
| `go test ./... -count=1 -v` | **24 PASS / 0 FAIL** — 1:1 with the 24 spec scenarios (repo 9, service 9, handler 6) |
| `go test -race ./...` | OK |
| `go test -cover ./...` | **89.0%** statements |

Edge cases explicitly tested:
- **pgx.ErrNoRows → 404** and, on the same GetByID error path, **non-ErrNoRows DB error (SQLSTATE 57P01) propagates raw** so it is masked as 500, never 404.
- **FK violation 23503 → `ErrInvalid` → 400**; **unique violation 23505 → `ErrConflict` → 409**.
- **Empty result → non-nil slice → HTTP body `[]`**, not `null` (asserted at both repo and handler layers).
- **Malformed JSON and unknown fields → 400** with `{"error":...}` (strict decoder + 1 MiB body limit).
- **Validation**: empty name, 129-char name, non-semver version, >1 MiB config, empty node_id → all `ErrInvalid`.
- **ID generation + timestamp stamping** on create; **negative paging clamped to (100, 0)** (observed in fake repo).
- **DELETE idempotency semantics**: 204 on first delete, 404 on repeat; handler returns 201 + `Location` on create.
- All mocks assert `ExpectationsWereMet()` (query arg/SQL exactness), and the compile-time `var _ PluginRepo = (*PGPluginRepo)(nil)` / `_ Queryer = (*pgxpool.Pool)(nil)` guards pass.
{"model": "deepseek-v4-flash", "problem_class": "go-pgx-repo-new-package", "result": "passed", "tests": 24}

Answer 2

GAP-002 fix: ship internal/plugin/ (pgx repo + service + HTTP handler) following the PGNodeRepo / graph_handler.go patterns, driven spec-first so the worker never needs to ask questions.

The sandbox contained no existing repo, so I re-implemented the pattern as a reference module at ~/plugin-demo and verified it. The spec-first dispatch artifact (SPEC-IMPL-GAP-002.md) pins the exact Go contracts and all 24 test scenarios before implementation — that is what made the delivery self-sufficient (single commit, zero rollbacks).

Key code — the contract that unblocks everything (internal/plugin/repo.go):

// PluginRepo mirrors PGNodeRepo: interface + pgx impl + sentinel errors.
type PluginRepo interface {
    Upsert(ctx context.Context, p *Plugin) error
    GetByID(ctx context.Context, id string) (*Plugin, error)
    ListByNode(ctx context.Context, nodeID string, limit, offset int) ([]*Plugin, error)
    Delete(ctx context.Context, id string) error
}

// Queryer is the narrow subset of *pgxpool.Pool the repo needs, so tests can
// inject pgxmock and DI can inject the real pool (guard below proves it).
type Queryer interface {
    Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
    Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
    QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}

var (
    _ PluginRepo = (*PGPluginRepo)(nil)
    _ Queryer    = (*pgxpool.Pool)(nil)  // compile-time guard PASS
)

Postgres-condition → sentinel translation (the error contract the handler depends on):

func (r *PGPluginRepo) Upsert(ctx context.Context, p *Plugin) error {
    _, err := r.q.Exec(ctx, sqlUpsertPlugin, p.ID, p.NodeID, p.Name, p.Version,
        p.Enabled, p.Config, p.CreatedAt, p.UpdatedAt)
    if err == nil { return nil }
    var pgErr *pgconn.PgError
    if errors.As(err, &pgErr) {
        switch pgErr.Code {
        case "23503": return ErrInvalid   // FK violation: node doesn't exist → 400
        case "23505": return ErrConflict  // unique violation → 409
        }
    }
    return err // raw → 500 (masked), never misreported as 404
}

GetByID maps pgx.ErrNoRows → ErrNotFound; Delete maps RowsAffected()==0 → ErrNotFound; ListByNode allocates make([]*Plugin, 0) so JSON emits [] never null.

Service (service.go) is the only layer that validates (name 1–128, semver regex, config ≤ 1 MiB), generates 32-hex IDs via crypto/rand, stamps UTC timestamps, and clamps paging (normalizePaging: limit default/max 100, offset floor 0).

Handler (handler.go, following graph_handler.go) — one struct + Routes(*http.ServeMux) (Go 1.22 {id} patterns), one writeErr mapping sentinel → 400/404/409, unknown → masked 500, no internal detail leakage:

func (h *Handler) Routes(mux *http.ServeMux) {
    mux.HandleFunc("POST /v1/plugins", h.Create)
    mux.HandleFunc("GET /v1/plugins/{id}", h.Get)
    mux.HandleFunc("GET /v1/nodes/{nodeID}/plugins", h.List)
    mux.HandleFunc("DELETE /v1/plugins/{id}", h.Delete)
}

Wiring (cmd/api/main.go): pgxpool.Pool → NewPGPluginRepo → NewService → NewHandler → Routes(mux).

Evidence & signatures

Verified in this sandbox (Go 1.26, deps via `go mod tidy` from proxy.golang.org):

| Check | Result |
|---|---|
| `go build ./...` (incl. `cmd/api`) | OK |
| `go vet ./...` | OK |
| `gofmt -l .` | clean |
| `go test ./... -count=1 -v` | **24 PASS / 0 FAIL** — 1:1 with the 24 spec scenarios (repo 9, service 9, handler 6) |
| `go test -race ./...` | OK |
| `go test -cover ./...` | **89.0%** statements |

Edge cases explicitly tested:
- **pgx.ErrNoRows → 404** and, on the same GetByID error path, **non-ErrNoRows DB error (SQLSTATE 57P01) propagates raw** so it is masked as 500, never 404.
- **FK violation 23503 → `ErrInvalid` → 400**; **unique violation 23505 → `ErrConflict` → 409**.
- **Empty result → non-nil slice → HTTP body `[]`**, not `null` (asserted at both repo and handler layers).
- **Malformed JSON and unknown fields → 400** with `{"error":...}` (strict decoder + 1 MiB body limit).
- **Validation**: empty name, 129-char name, non-semver version, >1 MiB config, empty node_id → all `ErrInvalid`.
- **ID generation + timestamp stamping** on create; **negative paging clamped to (100, 0)** (observed in fake repo).
- **DELETE idempotency semantics**: 204 on first delete, 404 on repeat; handler returns 201 + `Location` on create.
- All mocks assert `ExpectationsWereMet()` (query arg/SQL exactness), and the compile-time `var _ PluginRepo = (*PGPluginRepo)(nil)` / `_ Queryer = (*pgxpool.Pool)(nil)` guards pass.
{"model": "deepseek-v4-flash", "problem_class": "go-pgx-repo-new-package", "result": "passed", "tests": 24}
Generated from the verified corpus · MIT licensedBack to the catalog