Files
oikos/internal/httpapi/relationships.go
dtoro b02f94bfd4
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
feat: Phase 4 — RelationshipService, postgres RelRepo, converged edges
Problem: relationship create/end existed as three drifted copies
(HTTP CreateRelationship/EndRelationship, MCP create_relationship/
end_relationship) with inline SQL, no ontology edge validation on
either path, and no audit on the MCP path.

Change:
- Internal/adapters/postgres/repositories.go: RelRepo implements
  ports.RelationshipRepository (Create/End/ListFor) over the pool,
  with in-tx upsert + audit/event side effects on Create.
- Internal/core/app/relationships.go: RelationshipService validates
  edges against the cached ontology TypeTree (tree.ValidateEdge) and
  delegates the tx to the repository. The adapter resolves slug→entity
  and extracts types before calling the service.
- HTTP CreateRelationship: resolves source/target via ReadModels,
  passes resolved types to RelationshipService for edge validation.
  EndRelationship calls the service directly (audit stays in the
  adapter for End — a simple toggle with no ontology check).
- MCP create_relationship/end_relationship: rewired to the service
  (pool resolves entity IDs inline for the tool handlers; the service
  validates edges and writes audit). The MCP path now gets ontology
  validation and audit coverage for the first time.
- Composition root: RelationshipService built with RelRepo + Ontology
  and wired through httpapi.NewHandler, ListenAndServe, and MCP
  constructors.

Verification: go build/vet, full test suite (19 pkgs, DB integration
postgres+mcp green).
2026-08-16 00:09:40 +02:00

71 lines
1.9 KiB
Go

package httpapi
import (
"context"
"fmt"
"github.com/dtoro/oikos/internal/core/domain"
"github.com/dtoro/oikos/internal/core/app"
"github.com/dtoro/oikos/internal/httpapi/gen"
)
func (s *Server) CreateRelationship(ctx context.Context, req gen.CreateRelationshipRequestObject) (gen.CreateRelationshipResponseObject, error) {
if req.Body == nil {
return nil, fmt.Errorf("%w: request body is required", domain.ErrInvalidInput)
}
// Resolve source and target entities for their types (edge validation).
src, err := s.readModels.GetEntityBySlug(ctx, req.Body.Source)
if err != nil {
return nil, err
}
tgt, err := s.readModels.GetEntityBySlug(ctx, req.Body.Target)
if err != nil {
return nil, err
}
actorType, actor := actorInfo(ctx)
created, err := s.relService.Create(ctx, app.CreateRelationshipCmd{
SourceID: src.Entity.ID,
TargetID: tgt.Entity.ID,
SourceType: src.Entity.Type,
TargetType: tgt.Entity.Type,
Type: req.Body.Type,
Attributes: derefAttrs(req.Body.Attributes),
ActorType: actorType,
Actor: actor,
Method: "POST",
Path: "/api/v1/relationships",
})
if err != nil {
return nil, err
}
rel := gen.Relationship{
Source: req.Body.Source,
Target: req.Body.Target,
Type: created.Type,
ValidFrom: created.ValidFrom,
}
if req.Body.Attributes != nil {
rel.Attributes = req.Body.Attributes
}
return gen.CreateRelationship201JSONResponse(rel), nil
}
func (s *Server) EndRelationship(ctx context.Context, req gen.EndRelationshipRequestObject) (gen.EndRelationshipResponseObject, error) {
src, err := s.readModels.GetEntityBySlug(ctx, req.Params.Source)
if err != nil {
return nil, err
}
tgt, err := s.readModels.GetEntityBySlug(ctx, req.Params.Target)
if err != nil {
return nil, err
}
if err := s.relService.End(ctx, src.Entity.ID, tgt.Entity.ID, req.Params.RelType); err != nil {
return nil, err
}
return gen.EndRelationship204Response{}, nil
}