Add Phase 2 artifact repository: types, blob store, gRPC service

Build the complete artifact pillar with five packages:
- artifacts: Artifact, Snapshot, Citation, Publisher types with Get/Store
  DB methods, tag/category management, metadata ops, YAML import
- blob: content-addressable store (SHA256, hierarchical dir layout)
- proto: protobuf definitions (common.proto, artifacts.proto) with buf
  linting and code generation
- server: gRPC ArtifactService implementation (create/get artifacts,
  store/retrieve blobs, manage tags/categories, search by tag)

All FK insertion ordering is correct (parent rows before children).
Full test coverage across artifacts, blob, and server packages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-21 09:56:34 -07:00
parent bb2c7f7ef3
commit b64177baa8
22 changed files with 5017 additions and 1 deletions

70
artifacts/publisher.go Normal file
View File

@@ -0,0 +1,70 @@
package artifacts
import (
"context"
"database/sql"
"errors"
"fmt"
"git.wntrmute.dev/kyle/exo/core"
)
// Publisher represents a publishing entity.
type Publisher struct {
ID string
Name string
Address string
}
// findPublisher looks up a publisher by name and address, returning its ID.
func findPublisher(ctx context.Context, tx *sql.Tx, name, address string) (string, error) {
var id string
row := tx.QueryRowContext(ctx,
`SELECT id FROM publishers WHERE name=? AND address=?`, name, address)
if err := row.Scan(&id); err != nil {
return "", err
}
return id, nil
}
// Store persists a Publisher. If a publisher with the same name and address
// already exists, it reuses that record.
func (p *Publisher) Store(ctx context.Context, tx *sql.Tx) error {
if p.ID == "" {
id, err := findPublisher(ctx, tx, p.Name, p.Address)
if err == nil {
p.ID = id
return nil
}
if !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("artifacts: failed to look up publisher: %w", err)
}
p.ID = core.NewUUID()
}
_, err := tx.ExecContext(ctx,
`INSERT INTO publishers (id, name, address) VALUES (?, ?, ?)`,
p.ID, p.Name, p.Address)
if err != nil {
return fmt.Errorf("artifacts: failed to store publisher: %w", err)
}
return nil
}
// Get retrieves a Publisher by its ID.
func (p *Publisher) Get(ctx context.Context, tx *sql.Tx) (bool, error) {
if p.ID == "" {
return false, fmt.Errorf("artifacts: publisher missing ID: %w", core.ErrNoID)
}
row := tx.QueryRowContext(ctx,
`SELECT name, address FROM publishers WHERE id=?`, p.ID)
err := row.Scan(&p.Name, &p.Address)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, fmt.Errorf("artifacts: failed to look up publisher: %w", err)
}
return true, nil
}