Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
978 changes: 978 additions & 0 deletions generated/graphql/graph/executor/root_.generated.go

Large diffs are not rendered by default.

11,445 changes: 7,252 additions & 4,193 deletions generated/graphql/graph/executor/schema.generated.go

Large diffs are not rendered by default.

122 changes: 122 additions & 0 deletions generated/graphql/graph/model/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

38 changes: 38 additions & 0 deletions internal/api/graphql/graph/query/project_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func aggregatePopulatedFields(in []catalog.PopulatedField) []catalog.PopulatedFi
pivotColumns := make(map[key]map[string]struct{}, len(in))
pivotSelectors := make(map[key]map[string]struct{}, len(in))
extensionValues := make(map[key]map[string]catalog.ExtensionValueObservation, len(in))
semanticObservations := make(map[key]map[string]catalog.SemanticObservation, len(in))
for _, field := range in {
if strings.TrimSpace(field.ResourceType) == "" || strings.TrimSpace(field.Path) == "" || field.DocCount <= 0 {
continue
Expand All @@ -136,6 +137,7 @@ func aggregatePopulatedFields(in []catalog.PopulatedField) []catalog.PopulatedFi
pivotColumns[k] = make(map[string]struct{})
pivotSelectors[k] = make(map[string]struct{})
extensionValues[k] = make(map[string]catalog.ExtensionValueObservation)
semanticObservations[k] = make(map[string]catalog.SemanticObservation)
}
current.DocCount += field.DocCount
current.SampleCount = maxInt(current.SampleCount, field.SampleCount)
Expand All @@ -161,6 +163,32 @@ func aggregatePopulatedFields(in []catalog.PopulatedField) []catalog.PopulatedFi
observationKey := observation.URL + "\x00" + strings.Join(observation.URLPath, "\x00") + "\x00" + observation.SourcePath + "\x00" + observation.ValuePath + "\x00" + observation.ValueType
extensionValues[k][observationKey] = observation
}
for _, observation := range field.SemanticObservations {
observationKey := strings.Join([]string{observation.Source.Canonical, observation.Source.Type, observation.Source.Profile, observation.Source.Path, observation.Key.Selector, observation.Key.System, observation.Key.Code, observation.Key.Display, observation.Value.Selector, observation.Value.Type, observation.Cardinality, observation.RuleHint, observation.RuleVersion}, "\x00")
if existing, ok := semanticObservations[k][observationKey]; ok {
existing.Population += observation.Population
existing.ExamplesTruncated = existing.ExamplesTruncated || observation.ExamplesTruncated
exampleSet := make(map[string]struct{}, len(existing.Examples)+len(observation.Examples))
for _, value := range append(existing.Examples, observation.Examples...) {
if value != "" {
exampleSet[value] = struct{}{}
}
}
existing.Examples = existing.Examples[:0]
for value := range exampleSet {
existing.Examples = append(existing.Examples, value)
}
sort.Strings(existing.Examples)
if len(existing.Examples) > 32 {
existing.Examples = existing.Examples[:32]
existing.ExamplesTruncated = true
}
semanticObservations[k][observationKey] = existing
} else {
observation.Examples = append([]string(nil), observation.Examples...)
semanticObservations[k][observationKey] = observation
}
}
aggregates[k] = current
}
out := make([]catalog.PopulatedField, 0, len(aggregates))
Expand All @@ -177,13 +205,19 @@ func aggregatePopulatedFields(in []catalog.PopulatedField) []catalog.PopulatedFi
for _, value := range extensionValues[k] {
field.ExtensionValues = append(field.ExtensionValues, value)
}
for _, value := range semanticObservations[k] {
field.SemanticObservations = append(field.SemanticObservations, value)
}
sort.Strings(field.DistinctValues)
sort.Strings(field.PivotColumns)
sort.Strings(field.PivotValueSelectors)
sort.Slice(field.ExtensionValues, func(i, j int) bool {
left, right := field.ExtensionValues[i], field.ExtensionValues[j]
return left.URL+"\x00"+strings.Join(left.URLPath, "\x00")+"\x00"+left.SourcePath+"\x00"+left.ValuePath+"\x00"+left.ValueType < right.URL+"\x00"+strings.Join(right.URLPath, "\x00")+"\x00"+right.SourcePath+"\x00"+right.ValuePath+"\x00"+right.ValueType
})
sort.Slice(field.SemanticObservations, func(i, j int) bool {
return observationSortKey(field.SemanticObservations[i]) < observationSortKey(field.SemanticObservations[j])
})
field.SampleCount = maxInt(field.SampleCount, len(field.DistinctValues))
out = append(out, field)
}
Expand Down Expand Up @@ -218,6 +252,10 @@ func aggregatePopulatedReferences(in []catalog.PopulatedReference) []catalog.Pop
return out
}

func observationSortKey(observation catalog.SemanticObservation) string {
return strings.Join([]string{observation.Source.Canonical, observation.Key.Selector, observation.Key.System, observation.Key.Code, observation.Key.Display, observation.Value.Selector, observation.Value.Type, observation.Cardinality, observation.RuleHint, observation.RuleVersion}, "\x00")
}

func referenceLess(left, right catalog.PopulatedReference) bool {
if left.FromType != right.FromType {
return left.FromType < right.FromType
Expand Down
52 changes: 52 additions & 0 deletions internal/api/graphql/graph/query/semantic_catalog.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package queryapi

import (
"context"
"strings"

"github.com/calypr/loom/internal/authscope"
"github.com/calypr/loom/internal/catalog"
dataframeerrors "github.com/calypr/loom/internal/dataframe/errors"
"github.com/calypr/loom/internal/dataframe/semantic"
)

// SemanticCatalog reads only the active generation and caller-authorized
// catalog partitions. The root resource filter is applied at the catalog
// boundary so no concepts from another project or resource are disclosed.
func (s *Service) SemanticCatalog(ctx context.Context, req SemanticCatalogRequest) (*semantic.CatalogResult, error) {
project := strings.TrimSpace(req.Project)
root := strings.TrimSpace(req.RootResourceType)
if project == "" {
return nil, dataframeerrors.NewError(dataframeerrors.CodeProjectRequired, "")
}
if root == "" {
return nil, dataframeerrors.NewError(dataframeerrors.CodeRootResourceTypeRequired, "")
}
principal, _ := authscope.PrincipalFromContext(ctx)
if err := authscope.AuthorizeProject(principal, project, s.scopeResolver != nil); err != nil {
return nil, classifyError(err)
}
generation, err := s.resolveActiveGeneration(ctx, project)
if err != nil {
return nil, queryBackend(err)
}
scope, err := s.resolveReadScopeForGeneration(ctx, principal, project, generation, req.AuthResourcePaths)
if err != nil {
return nil, queryBackend(err)
}
fields, err := s.discoverFields(ctx, catalog.PopulatedFieldOptions{
Project: project, DatasetGeneration: generation, ResourceType: root,
AuthResourcePathsUnrestricted: catalog.ExplicitAuthResourcePathsUnrestricted(scope.Unrestricted()),
AuthResourcePaths: cloneStrings(scope.AuthResourcePaths),
})
if err != nil {
return nil, queryBackend(err)
}
fields = aggregatePopulatedFields(fields)
result := semantic.DiscoverCatalog(fields, semantic.CatalogOptions{
Project: project, SourceGeneration: generation, ResourceType: root,
ResourceLimit: req.ResourceLimit, ConceptLimitPerResource: req.ConceptLimitPerResource,
})
result.AuthResourcePaths = cloneStrings(scope.AuthResourcePaths)
return &result, nil
}
42 changes: 42 additions & 0 deletions internal/api/graphql/graph/query/semantic_catalog_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package queryapi

import (
"context"
"testing"

"github.com/calypr/loom/internal/authscope"
"github.com/calypr/loom/internal/catalog"
)

func TestServiceSemanticCatalogIsRootAndAuthScoped(t *testing.T) {
var got catalog.PopulatedFieldOptions
service := NewService(Config{
ActiveManifestResolver: &builderActiveManifestResolver{manifest: builderReadyManifest(t, "P1", "generation-1")},
DiscoverFields: func(_ context.Context, opts catalog.PopulatedFieldOptions) ([]catalog.PopulatedField, error) {
got = opts
return []catalog.PopulatedField{{Project: "P1", ResourceType: opts.ResourceType, Path: "code", DocCount: 3, SemanticObservations: []catalog.SemanticObservation{{Source: catalog.SemanticObservationSource{Canonical: "Observation.code", Type: "Observation", Path: "code"}, Key: catalog.SemanticObservationKey{Selector: "code.coding[]", Code: "x"}, Value: catalog.SemanticObservationValue{Selector: "valueString", Type: "string"}, Cardinality: "single", Population: 2, Examples: []string{"safe"}, RuleHint: "OBSERVATION_CODE_VALUE", RuleVersion: "1"}}}}, nil
},
})
ctx := authscope.ContextWithPrincipal(context.Background(), &authscope.Principal{Subject: "u1", Projects: []string{"P1"}, AuthResourcePaths: []string{"a"}})
resp, err := service.SemanticCatalog(ctx, SemanticCatalogRequest{Project: "P1", RootResourceType: "Observation"})
if err != nil {
t.Fatal(err)
}
if got.ResourceType != "Observation" || len(got.AuthResourcePaths) != 1 || got.AuthResourcePaths[0] != "a" {
t.Fatalf("scope/root disclosure: %+v", got)
}
if len(resp.Resources) != 1 || resp.Resources[0].ResourceType != "Observation" || resp.Resources[0].Families[0].Concepts[0].Examples.Values[0] != "safe" {
t.Fatalf("unexpected catalog: %#v", resp)
}
}

func TestServiceSemanticCatalogRejectsUnauthorizedProjectAndPath(t *testing.T) {
service := NewService(Config{})
ctx := authscope.ContextWithPrincipal(context.Background(), &authscope.Principal{Subject: "u1", Projects: []string{"P1"}, AuthResourcePaths: []string{"a"}})
if _, err := service.SemanticCatalog(ctx, SemanticCatalogRequest{Project: "P2", RootResourceType: "Observation"}); err == nil {
t.Fatal("expected project authorization error")
}
if _, err := service.SemanticCatalog(ctx, SemanticCatalogRequest{Project: "P1", RootResourceType: "Observation", AuthResourcePaths: []string{"secret"}}); err == nil {
t.Fatal("expected path authorization error")
}
}
8 changes: 8 additions & 0 deletions internal/api/graphql/graph/query/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ type ResourceHints struct {
Traversals []catalog.PopulatedReference
}

type SemanticCatalogRequest struct {
Project string
RootResourceType string
AuthResourcePaths []string
ResourceLimit int
ConceptLimitPerResource int
}

type RelatedResourceHints struct {
ViaLabel string
EdgeCount int64
Expand Down
20 changes: 20 additions & 0 deletions internal/api/graphql/graph/resolver/schema.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading