Files
fn_registry/fn_operations/operations.go
T
egutierrez 0095de2ce7 feat: CheckSnapshots y UpdateSnapshot en fn_operations library
Añade UpdateTypeSnapshot al store, CheckSnapshots para comparar snapshots
locales vs registry (up_to_date/outdated/missing), y UpdateSnapshot para
re-snapshot con retorno de old/new para diffing.
2026-03-28 13:41:35 +01:00

291 lines
7.5 KiB
Go

package fn_operations
import (
"fmt"
"time"
"fn-registry/registry"
)
// InsertEntityWithSnapshot inserts an entity, snapshotting its type from the registry if needed.
// registryDB can be nil if the type is already snapshotted.
func InsertEntityWithSnapshot(opsDB *DB, registryDB *registry.DB, e *Entity) error {
if err := ValidateEntity(e); err != nil {
return err
}
// Check if type is already snapshotted
snap, err := opsDB.GetTypeSnapshot(e.TypeRef)
if err != nil {
return fmt.Errorf("checking type snapshot: %w", err)
}
if snap == nil {
// Need to fetch from registry
if registryDB == nil {
return fmt.Errorf("type %q not found in local snapshots and no registry provided", e.TypeRef)
}
if err := SnapshotType(opsDB, registryDB, e.TypeRef); err != nil {
return err
}
}
return opsDB.InsertEntity(e)
}
// SnapshotType fetches a type from the registry and copies it to types_snapshot.
func SnapshotType(opsDB *DB, registryDB *registry.DB, typeID string) error {
t, err := registryDB.GetType(typeID)
if err != nil {
return fmt.Errorf("fetching type %q from registry: %w", typeID, err)
}
snap := &TypeSnapshot{
ID: t.ID,
Version: t.Version,
Lang: t.Lang,
Algebraic: string(t.Algebraic),
Definition: t.Definition,
Description: t.Description,
SnappedAt: time.Now().UTC(),
}
return opsDB.InsertTypeSnapshot(snap)
}
// InsertRelationSafe validates, checks for cycles, and inserts a relation.
func InsertRelationSafe(db *DB, r *Relation) error {
entities, err := buildEntitySet(db)
if err != nil {
return err
}
if err := ValidateRelation(r, entities); err != nil {
return err
}
// from_entity is required when not using relation_inputs
if r.FromEntity == "" {
return fmt.Errorf("from_entity is required (use InsertRelationWithInputs for multi-input relations)")
}
// Cycle detection only for causal relations
if r.Via != "" {
if err := DetectCycle(db, r.FromEntity, r.ToEntity); err != nil {
return err
}
}
return db.InsertRelation(r)
}
// InsertRelationWithInputs validates and inserts a relation with multiple inputs in a transaction.
func InsertRelationWithInputs(db *DB, r *Relation, inputs []RelationInput) error {
entities, err := buildEntitySet(db)
if err != nil {
return err
}
if err := ValidateRelation(r, entities); err != nil {
return err
}
if err := ValidateRelationInputs(inputs, entities); err != nil {
return err
}
// Cycle detection for each input if causal
if r.Via != "" {
for _, input := range inputs {
if err := DetectCycle(db, input.EntityID, r.ToEntity); err != nil {
return err
}
}
}
tx, err := db.Conn().Begin()
if err != nil {
return fmt.Errorf("beginning transaction: %w", err)
}
defer tx.Rollback()
// Insert relation
now := time.Now().UTC()
if r.CreatedAt.IsZero() {
r.CreatedAt = now
}
r.UpdatedAt = now
var startedAt, endedAt *string
if r.StartedAt != nil {
s := r.StartedAt.Format(time.RFC3339)
startedAt = &s
}
if r.EndedAt != nil {
s := r.EndedAt.Format(time.RFC3339)
endedAt = &s
}
_, err = tx.Exec(`
INSERT OR REPLACE INTO relations (id, name, from_entity, to_entity, via, description, purity, direction, weight, status, started_at, ended_at, "order", tags, notes, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
r.ID, r.Name, r.FromEntity, r.ToEntity, r.Via, r.Description,
r.Purity, string(r.Direction), r.Weight, string(r.Status),
startedAt, endedAt, r.Order, marshalStrings(r.Tags), r.Notes,
r.CreatedAt.Format(time.RFC3339), r.UpdatedAt.Format(time.RFC3339),
)
if err != nil {
return fmt.Errorf("inserting relation: %w", err)
}
// Insert inputs
for _, ri := range inputs {
_, err = tx.Exec(`
INSERT INTO relation_inputs (id, relation_id, entity_id, role, "order")
VALUES (?, ?, ?, ?, ?)`,
ri.ID, ri.RelationID, ri.EntityID, ri.Role, ri.Order,
)
if err != nil {
return fmt.Errorf("inserting relation_input: %w", err)
}
}
return tx.Commit()
}
// Graph holds the full entity-relation graph for a project.
type Graph struct {
Entities []Entity
Relations []Relation
Inputs map[string][]RelationInput
}
// GetEntityGraph returns all entities and relations for visualization.
func GetEntityGraph(db *DB) (*Graph, error) {
entities, err := db.ListEntities("", "")
if err != nil {
return nil, fmt.Errorf("listing entities: %w", err)
}
relations, err := db.ListRelations("")
if err != nil {
return nil, fmt.Errorf("listing relations: %w", err)
}
inputs := map[string][]RelationInput{}
for _, r := range relations {
ri, err := db.GetRelationInputs(r.ID)
if err != nil {
return nil, fmt.Errorf("getting inputs for relation %s: %w", r.ID, err)
}
if len(ri) > 0 {
inputs[r.ID] = ri
}
}
return &Graph{
Entities: entities,
Relations: relations,
Inputs: inputs,
}, 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 {
return nil, fmt.Errorf("building entity set: %w", err)
}
set := make(map[string]bool, len(all))
for _, e := range all {
set[e.ID] = true
}
return set, nil
}