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 } 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 }