up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Mirror Thin Bundle Sign & Verify / mirror-sign (push) Has been cancelled
Signals CI & Image / signals-ci (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-26 07:47:08 +02:00
parent 56e2f64d07
commit 1c782897f7
184 changed files with 8991 additions and 649 deletions

View File

@@ -5,10 +5,12 @@ namespace StellaOps.Graph.Api.Services;
public sealed class InMemoryGraphRepository
{
private readonly List<NodeTile> _nodes;
private readonly List<EdgeTile> _edges;
private readonly Dictionary<string, (List<NodeTile> Nodes, List<EdgeTile> Edges)> _snapshots;
public InMemoryGraphRepository()
public InMemoryGraphRepository(IEnumerable<NodeTile>? seed = null, IEnumerable<EdgeTile>? edges = null)
{
_nodes = new List<NodeTile>
_nodes = seed?.ToList() ?? new List<NodeTile>
{
new() { Id = "gn:acme:component:example", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:npm/example@1.0.0", ["ecosystem"] = "npm" } },
new() { Id = "gn:acme:component:widget", Kind = "component", Tenant = "acme", Attributes = new() { ["purl"] = "pkg:npm/widget@2.0.0", ["ecosystem"] = "npm" } },
@@ -17,16 +19,26 @@ public sealed class InMemoryGraphRepository
new() { Id = "gn:bravo:component:widget", Kind = "component", Tenant = "bravo",Attributes = new() { ["purl"] = "pkg:npm/widget@2.0.0", ["ecosystem"] = "npm" } },
new() { Id = "gn:bravo:artifact:sha256:def", Kind = "artifact", Tenant = "bravo",Attributes = new() { ["digest"] = "sha256:def", ["ecosystem"] = "container" } },
};
_edges = edges?.ToList() ?? new List<EdgeTile>
{
new() { Id = "ge:acme:artifact->component", Kind = "builds", Tenant = "acme", Source = "gn:acme:artifact:sha256:abc", Target = "gn:acme:component:example", Attributes = new() { ["reason"] = "sbom" } },
new() { Id = "ge:acme:component->component", Kind = "depends_on", Tenant = "acme", Source = "gn:acme:component:example", Target = "gn:acme:component:widget", Attributes = new() { ["scope"] = "runtime" } },
new() { Id = "ge:bravo:artifact->component", Kind = "builds", Tenant = "bravo", Source = "gn:bravo:artifact:sha256:def", Target = "gn:bravo:component:widget", Attributes = new() { ["reason"] = "sbom" } },
};
// Drop edges whose endpoints aren't present in the current node set to avoid invalid graph seeds in tests.
var nodeIds = _nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
_edges = _edges.Where(e => nodeIds.Contains(e.Source) && nodeIds.Contains(e.Target)).ToList();
_snapshots = SeedSnapshots();
}
public IEnumerable<NodeTile> Query(string tenant, GraphSearchRequest request)
{
var limit = Math.Clamp(request.Limit ?? 50, 1, 500);
var cursorOffset = CursorCodec.Decode(request.Cursor);
var queryable = _nodes
.Where(n => n.Tenant.Equals(tenant, StringComparison.Ordinal))
.Where(n => request.Kinds.Contains(n.Kind, StringComparer.OrdinalIgnoreCase));
.Where(n => request.Kinds is null || request.Kinds.Length == 0 || request.Kinds.Contains(n.Kind, StringComparer.OrdinalIgnoreCase));
if (!string.IsNullOrWhiteSpace(request.Query))
{
@@ -38,13 +50,82 @@ public sealed class InMemoryGraphRepository
queryable = queryable.Where(n => FiltersMatch(n, request.Filters!));
}
queryable = request.Ordering switch
return queryable;
}
public (IReadOnlyList<NodeTile> Nodes, IReadOnlyList<EdgeTile> Edges) QueryGraph(string tenant, GraphQueryRequest request)
{
var nodes = Query(tenant, new GraphSearchRequest
{
"id" => queryable.OrderBy(n => n.Id, StringComparer.Ordinal),
_ => queryable.OrderBy(n => n.Id.Length).ThenBy(n => n.Id, StringComparer.Ordinal)
Kinds = request.Kinds,
Query = request.Query,
Filters = request.Filters,
Limit = request.Limit,
Cursor = request.Cursor
}).ToList();
var nodeIds = nodes.Select(n => n.Id).ToHashSet(StringComparer.Ordinal);
var edges = request.IncludeEdges
? _edges.Where(e => e.Tenant.Equals(tenant, StringComparison.Ordinal) && nodeIds.Contains(e.Source) && nodeIds.Contains(e.Target))
.OrderBy(e => e.Id, StringComparer.Ordinal)
.ToList()
: new List<EdgeTile>();
return (nodes, edges);
}
public (IReadOnlyList<NodeTile> Nodes, IReadOnlyList<EdgeTile> Edges)? GetSnapshot(string tenant, string snapshotId)
{
if (_snapshots.TryGetValue($"{tenant}:{snapshotId}", out var snap))
{
return (snap.Nodes, snap.Edges);
}
return null;
}
private Dictionary<string, (List<NodeTile> Nodes, List<EdgeTile> Edges)> SeedSnapshots()
{
var dict = new Dictionary<string, (List<NodeTile>, List<EdgeTile>)>(StringComparer.Ordinal);
dict["acme:snapA"] = (new List<NodeTile>(_nodes), new List<EdgeTile>(_edges));
var updatedNodes = new List<NodeTile>(_nodes.Select(n => n with
{
Attributes = new Dictionary<string, object?>(n.Attributes)
}));
var widget = updatedNodes.FirstOrDefault(n => n.Id == "gn:acme:component:widget");
if (widget is null)
{
// Custom seeds may not include the default widget node; skip optional snapshot wiring in that case.
return dict;
}
widget.Attributes["purl"] = "pkg:npm/widget@2.1.0";
updatedNodes.Add(new NodeTile
{
Id = "gn:acme:component:newlib",
Kind = "component",
Tenant = "acme",
Attributes = new() { ["purl"] = "pkg:npm/newlib@1.0.0", ["ecosystem"] = "npm" }
});
var updatedEdges = new List<EdgeTile>(_edges)
{
new()
{
Id = "ge:acme:component->component:new",
Kind = "depends_on",
Tenant = "acme",
Source = widget.Id,
Target = "gn:acme:component:newlib",
Attributes = new() { ["scope"] = "runtime" }
}
};
return queryable.Skip(cursorOffset).Take(limit + 1).ToArray();
dict["acme:snapB"] = (updatedNodes, updatedEdges);
return dict;
}
private static bool MatchesQuery(NodeTile node, string query)