package app import ( "context" "fmt" "time" "github.com/dtoro/oikos/internal/core/domain" "github.com/dtoro/oikos/internal/core/ports" ) // RelationshipService owns the relationship aggregate's use-cases: create // with ontology edge validation, soft-delete, and list reads. The adapter // resolves slug→entity and extracts types before calling the service; the // service validates endpoint types against the (cached) ontology. type RelationshipService struct { rels ports.RelationshipRepository onto ports.OntologyStore } // NewRelationshipService wires the service. func NewRelationshipService(rels ports.RelationshipRepository, onto ports.OntologyStore) *RelationshipService { return &RelationshipService{rels: rels, onto: onto} } // CreateRelationshipCmd is one edge creation. SourceType and TargetType are // the resolved entity types (used for ontology edge validation). type CreateRelationshipCmd struct { SourceID domain.UUID TargetID domain.UUID SourceType string TargetType string Type string Attributes map[string]any ActorType string Actor string Method string Path string } // Create validates the edge against the ontology and creates it in one // transaction. func (s *RelationshipService) Create(ctx context.Context, cmd CreateRelationshipCmd) (domain.Relationship, error) { tree, err := s.onto.LoadTypeTree(ctx) if err != nil { return domain.Relationship{}, err } if err := tree.ValidateEdge(cmd.Type, cmd.SourceType, cmd.TargetType); err != nil { return domain.Relationship{}, fmt.Errorf("invalid edge: %w", err) } rel := domain.Relationship{ SourceID: cmd.SourceID, TargetID: cmd.TargetID, Type: cmd.Type, Attributes: cmd.Attributes, ValidFrom: time.Now(), } created, err := s.rels.Create(ctx, ports.RelationshipCreateInput{ Relationship: rel, Audit: []ports.AuditEntry{{ ActorType: cmd.ActorType, ActorLabel: cmd.Actor, Action: "create", EntityID: cmd.SourceID, Method: cmd.Method, Path: cmd.Path, Details: map[string]any{"source": cmd.SourceID, "target": cmd.TargetID, "type": cmd.Type}, }}, }) if err != nil { return domain.Relationship{}, err } return created, nil } // End terminates an active relationship (soft-delete). Audit is kept at the // adapter level since End is a simple state toggle with no ontology check. func (s *RelationshipService) End(ctx context.Context, source, target domain.UUID, relType string) error { return s.rels.End(ctx, source, target, relType) }