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

145
artifacts/snapshot.go Normal file
View File

@@ -0,0 +1,145 @@
package artifacts
import (
"context"
"database/sql"
"fmt"
"time"
"git.wntrmute.dev/kyle/exo/blob"
"git.wntrmute.dev/kyle/exo/core"
"git.wntrmute.dev/kyle/exo/db"
)
// MIME makes explicit where a MIME type is expected.
type MIME string
// BlobRef is a reference to a blob in the content-addressable store.
type BlobRef struct {
SnapshotID string
ID string // SHA256 hash
Format MIME
Data []byte // in-memory content (nil when loaded from DB)
}
// Store persists a BlobRef's metadata in the database and writes its data
// to the blob store (if data is present).
func (b *BlobRef) Store(ctx context.Context, tx *sql.Tx, store *blob.Store) error {
if b.Data != nil && store != nil {
id, err := store.Write(b.Data)
if err != nil {
return fmt.Errorf("artifacts: failed to write blob to store: %w", err)
}
b.ID = id
}
_, err := tx.ExecContext(ctx,
`INSERT INTO blobs (snapshot_id, id, format) VALUES (?, ?, ?)`,
b.SnapshotID, b.ID, string(b.Format))
if err != nil {
return fmt.Errorf("artifacts: failed to store blob ref: %w", err)
}
return nil
}
// Snapshot represents content at a specific point in time or format.
type Snapshot struct {
ArtifactID string
ID string
StoreDate time.Time
Datetime time.Time
Citation *Citation
Source string
Blobs map[MIME]*BlobRef
Metadata core.Metadata
}
// Store persists a Snapshot and its blobs.
func (snap *Snapshot) Store(ctx context.Context, tx *sql.Tx, store *blob.Store) error {
if snap.Citation != nil {
if err := snap.Citation.Store(ctx, tx); err != nil {
return fmt.Errorf("artifacts: failed to store snapshot citation: %w", err)
}
}
citationID := ""
if snap.Citation != nil {
citationID = snap.Citation.ID
}
// Insert the snapshot row first so FK-dependent rows (blobs, metadata) can reference it.
_, err := tx.ExecContext(ctx,
`INSERT INTO artifact_snapshots (artifact_id, id, stored_at, datetime, citation_id, source) VALUES (?, ?, ?, ?, ?, ?)`,
snap.ArtifactID, snap.ID, snap.StoreDate.Unix(), db.ToDBTime(snap.Datetime), citationID, snap.Source)
if err != nil {
return fmt.Errorf("artifacts: failed to store snapshot: %w", err)
}
if err := StoreMetadata(ctx, tx, snap.ID, snap.Metadata); err != nil {
return fmt.Errorf("artifacts: failed to store snapshot metadata: %w", err)
}
for _, b := range snap.Blobs {
b.SnapshotID = snap.ID
if err := b.Store(ctx, tx, store); err != nil {
return fmt.Errorf("artifacts: failed to store snapshot blob: %w", err)
}
}
return nil
}
// Get retrieves a Snapshot by its ID, including blobs and metadata.
func (snap *Snapshot) Get(ctx context.Context, tx *sql.Tx) error {
if snap.ID == "" {
return fmt.Errorf("artifacts: snapshot missing ID: %w", core.ErrNoID)
}
snap.Citation = &Citation{}
var datetime string
var stored int64
row := tx.QueryRowContext(ctx,
`SELECT artifact_id, stored_at, datetime, citation_id, source FROM artifact_snapshots WHERE id=?`,
snap.ID)
err := row.Scan(&snap.ArtifactID, &stored, &datetime, &snap.Citation.ID, &snap.Source)
if err != nil {
return fmt.Errorf("artifacts: failed to retrieve snapshot: %w", err)
}
snap.StoreDate = time.Unix(stored, 0)
snap.Datetime, err = db.FromDBTime(datetime, nil)
if err != nil {
return err
}
if err := snap.Citation.Get(ctx, tx); err != nil {
return fmt.Errorf("artifacts: failed to retrieve snapshot citation: %w", err)
}
snap.Metadata, err = GetMetadata(ctx, tx, snap.ID)
if err != nil {
return err
}
// Load blob references.
snap.Blobs = map[MIME]*BlobRef{}
rows, err := tx.QueryContext(ctx,
`SELECT id, format FROM blobs WHERE snapshot_id=?`, snap.ID)
if err != nil {
return fmt.Errorf("artifacts: failed to retrieve snapshot blobs: %w", err)
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var id, format string
if err := rows.Scan(&id, &format); err != nil {
return fmt.Errorf("artifacts: failed to scan blob: %w", err)
}
snap.Blobs[MIME(format)] = &BlobRef{
SnapshotID: snap.ID,
ID: id,
Format: MIME(format),
}
}
return rows.Err()
}