diff --git a/fn_operations/operations.go b/fn_operations/operations.go index d52fc9a5..973c3f64 100644 --- a/fn_operations/operations.go +++ b/fn_operations/operations.go @@ -189,6 +189,94 @@ func GetEntityGraph(db *DB) (*Graph, error) { }, nil } +// SnapshotStatus describes the state of a snapshot vs the registry. +type SnapshotStatus string + +const ( + SnapshotUpToDate SnapshotStatus = "up_to_date" + SnapshotOutdated SnapshotStatus = "outdated" + SnapshotMissing SnapshotStatus = "missing" // exists locally but not in registry +) + +// SnapshotCheckResult holds the comparison for one type snapshot. +type SnapshotCheckResult struct { + ID string + LocalVersion string + RegistryVersion string + Status SnapshotStatus +} + +// CheckSnapshots compares all local snapshots against the registry. +func CheckSnapshots(opsDB *DB, registryDB *registry.DB) ([]SnapshotCheckResult, error) { + snaps, err := opsDB.ListTypeSnapshots() + if err != nil { + return nil, fmt.Errorf("listing snapshots: %w", err) + } + + var results []SnapshotCheckResult + for _, snap := range snaps { + regType, err := registryDB.GetType(snap.ID) + if err != nil { + // Not found in registry + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + Status: SnapshotMissing, + }) + continue + } + + status := SnapshotUpToDate + if regType.Version != snap.Version { + status = SnapshotOutdated + } + + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + RegistryVersion: regType.Version, + Status: status, + }) + } + return results, nil +} + +// UpdateSnapshot re-snapshots a type from the registry, replacing the local copy. +// Returns the old and new definitions for diffing. +func UpdateSnapshot(opsDB *DB, registryDB *registry.DB, typeID string) (old, new_ *TypeSnapshot, err error) { + // Get current local snapshot + oldSnap, err := opsDB.GetTypeSnapshot(typeID) + if err != nil { + return nil, nil, fmt.Errorf("reading local snapshot: %w", err) + } + if oldSnap == nil { + return nil, nil, fmt.Errorf("type %q not found in local snapshots", typeID) + } + + // Get current registry type + regType, err := registryDB.GetType(typeID) + if err != nil { + return nil, nil, fmt.Errorf("fetching type %q from registry: %w", typeID, err) + } + + // Build new snapshot + newSnap := &TypeSnapshot{ + ID: regType.ID, + Version: regType.Version, + Lang: regType.Lang, + Algebraic: string(regType.Algebraic), + Definition: regType.Definition, + Description: regType.Description, + SnappedAt: time.Now().UTC(), + } + + if err := opsDB.UpdateTypeSnapshot(newSnap); err != nil { + return nil, nil, fmt.Errorf("updating snapshot: %w", err) + } + + return oldSnap, newSnap, nil +} + func buildEntitySet(db *DB) (map[string]bool, error) { all, err := db.ListEntities("", "") if err != nil { diff --git a/fn_operations/store.go b/fn_operations/store.go index 52f2389a..ed1f47c3 100644 --- a/fn_operations/store.go +++ b/fn_operations/store.go @@ -58,6 +58,20 @@ func (db *DB) InsertTypeSnapshot(ts *TypeSnapshot) error { return err } +// UpdateTypeSnapshot replaces an existing snapshot with a new version. +func (db *DB) UpdateTypeSnapshot(ts *TypeSnapshot) error { + if ts.SnappedAt.IsZero() { + ts.SnappedAt = time.Now().UTC() + } + _, err := db.conn.Exec(` + UPDATE types_snapshot SET version=?, lang=?, algebraic=?, definition=?, description=?, snapped_at=? + WHERE id=?`, + ts.Version, ts.Lang, ts.Algebraic, ts.Definition, ts.Description, + ts.SnappedAt.Format(time.RFC3339), ts.ID, + ) + return err +} + // GetTypeSnapshot returns a type snapshot by ID. func (db *DB) GetTypeSnapshot(id string) (*TypeSnapshot, error) { row := db.conn.QueryRow("SELECT id, version, lang, algebraic, definition, description, snapped_at FROM types_snapshot WHERE id = ?", id)