Skip to content

Commit

Permalink
[ODS-4799] Introduce resource POST/Retry order in dependency endpoint (
Browse files Browse the repository at this point in the history
  • Loading branch information
mjaksn authored Sep 13, 2024
1 parent 7e5e035 commit 3b1708c
Show file tree
Hide file tree
Showing 12 changed files with 442 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Linq;
using EdFi.Ods.Common.Conventions;
using EdFi.Ods.Common.Exceptions;
using EdFi.Ods.Common.Extensions;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Models.Graphs;
using EdFi.Ods.Common.Models.Resource;
Expand All @@ -17,9 +18,10 @@ namespace EdFi.Ods.Api.Security.Authorization
{
public class PersonResourceLoadGraphTransformer : IResourceLoadGraphTransformer
{
private const string TransformationErrorText = "Dependency graph transformation for security considerations failed. See log for more details.";
private const string TransformationErrorText =
"Dependency graph transformation for security considerations failed. See log for more details.";
private readonly ILog _logger = LogManager.GetLogger(typeof(PersonResourceLoadGraphTransformer));

public void Transform(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
{
ApplyStudentTransformation(resourceGraph);
Expand All @@ -30,16 +32,19 @@ public void Transform(BidirectionalGraph<Resource, AssociationViewEdge> resource
private void ApplyStaffTransformation(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
{
var resources = resourceGraph.Vertices.ToList();

var staffResource = resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Staff"));

var staffResource =
resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Staff"));

var staffEdOrgEmployAssoc =
resources.FirstOrDefault(
x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationEmploymentAssociation"));
x => x.FullName == new FullName(
EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationEmploymentAssociation"));

var staffEdOrgAssignAssoc =
resources.FirstOrDefault(
x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationAssignmentAssociation"));
x => x.FullName == new FullName(
EdFiConventions.PhysicalSchemaName, "StaffEducationOrganizationAssignmentAssociation"));

// No staff entity in the graph, nothing to do.
if (staffResource == null)
Expand All @@ -49,7 +54,8 @@ private void ApplyStaffTransformation(BidirectionalGraph<Resource, AssociationVi

if (staffEdOrgEmployAssoc == null && staffEdOrgAssignAssoc == null)
{
var message = "Unable to transform resource load graph since StaffEducationOrganizationAssignmentAssociation and StaffEducationOrganizationEmploymentAssociation were not found in the graph.";
var message =
"Unable to transform resource load graph since StaffEducationOrganizationAssignmentAssociation and StaffEducationOrganizationEmploymentAssociation were not found in the graph.";

_logger.Error(message);

Expand All @@ -63,24 +69,38 @@ private void ApplyStaffTransformation(BidirectionalGraph<Resource, AssociationVi
var directStaffDependencies = resourceGraph.OutEdges(staffResource)
.Where(e => e.Target != staffEdOrgEmployAssoc && e.Target != staffEdOrgAssignAssoc)
.ToList();

// Add dependency on primaryRelationship path
foreach (var directStaffDependency in directStaffDependencies)
{
// Re-point the edge to the primary relationships
resourceGraph.RemoveEdge(directStaffDependency);

resourceGraph.AddEdge(new AssociationViewEdge(staffEdOrgAssignAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));
resourceGraph.AddEdge(new AssociationViewEdge(staffEdOrgEmployAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));

resourceGraph.AddEdge(
new AssociationViewEdge(
staffEdOrgAssignAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));

resourceGraph.AddEdge(
new AssociationViewEdge(
staffEdOrgEmployAssoc, directStaffDependency.Target, directStaffDependency.AssociationView));
}

// Add StaffEducationOrganizationAssignmentAssociations/#POSTRetry node for Staff resource
AddPostRetryVertexForResource(resourceGraph, staffEdOrgAssignAssoc, staffResource);

// Add StaffEducationOrganizationEmploymentAssociations/#POSTRetry node for Staff resource
AddPostRetryVertexForResource(resourceGraph, staffEdOrgEmployAssoc, staffResource);
}

private static void ApplyStudentTransformation(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
{
var resources = resourceGraph.Vertices.ToList();

var studentResource = resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Student"));
var studentSchoolAssociationResource = resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "StudentSchoolAssociation"));
var studentResource =
resources.FirstOrDefault(x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Student"));

var studentSchoolAssociationResource = resources.FirstOrDefault(
x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "StudentSchoolAssociation"));

// No student entity in the graph, nothing to do.
if (studentResource == null)
Expand Down Expand Up @@ -108,18 +128,26 @@ private static void ApplyStudentTransformation(BidirectionalGraph<Resource, Asso
{
// Re-point the edge to the primary relationships
resourceGraph.RemoveEdge(directStudentDependency);
resourceGraph.AddEdge(new AssociationViewEdge(studentSchoolAssociationResource, directStudentDependency.Target, directStudentDependency.AssociationView));

resourceGraph.AddEdge(
new AssociationViewEdge(
studentSchoolAssociationResource, directStudentDependency.Target,
directStudentDependency.AssociationView));
}

// Add a StudentSchoolAssociation/#POSTRetry node for Student resource
AddPostRetryVertexForResource(resourceGraph, studentSchoolAssociationResource, studentResource);
}

private void ApplyContactTransformation(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph)
{
var resources = resourceGraph.Vertices.ToList();

var contactResource = resources.FirstOrDefault(x =>
x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Contact")
|| x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Parent"));

var contactResource = resources.FirstOrDefault(
x =>
x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Contact")
|| x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, "Parent"));

// No entity named Parent or Contact in the graph, nothing to do.
if (contactResource == null)
{
Expand All @@ -135,10 +163,12 @@ private void ApplyContactTransformation(BidirectionalGraph<Resource, Association

var studentContactAssociationResource = resources.FirstOrDefault(
x => x.FullName == new FullName(EdFiConventions.PhysicalSchemaName, contactStudentAssociationName));

if (studentContactAssociationResource == null)
{
string message = $"Unable to transform resource load graph as {contactStudentAssociationName} was not found in the graph.";
string message =
$"Unable to transform resource load graph as {contactStudentAssociationName} was not found in the graph.";

_logger.Error(message);

throw new SecurityAuthorizationException(
Expand All @@ -157,12 +187,21 @@ private void ApplyContactTransformation(BidirectionalGraph<Resource, Association
{
// Re-point the edge to the primary relationships
resourceGraph.RemoveEdge(directContactDependency);
resourceGraph.AddEdge(new AssociationViewEdge(studentContactAssociationResource, directContactDependency.Target, directContactDependency.AssociationView));
}

resourceGraph.AddEdge(
new AssociationViewEdge(
studentContactAssociationResource, directContactDependency.Target,
directContactDependency.AssociationView));
}

// Add a StudentContactAssociationResource/#POSTRetry node for Contact resource
AddPostRetryVertexForResource(resourceGraph, studentContactAssociationResource, contactResource);

string LogAndThrowException()
{
string message = $"Unable to transform resource load graph as a student association for {contactResource.FullName} is not defined.";
string message =
$"Unable to transform resource load graph as a student association for {contactResource.FullName} is not defined.";

_logger.Error(message);

throw new SecurityAuthorizationException(
Expand All @@ -171,5 +210,18 @@ string LogAndThrowException()
message: message);
}
}

private static void AddPostRetryVertexForResource(BidirectionalGraph<Resource, AssociationViewEdge> resourceGraph, Resource postRetrySource,
Resource postRetryTarget)
{
var postRetryVertex = new Resource(postRetrySource.Name);
postRetryVertex.IsPostRetryResource = true;

postRetryVertex.PostRetryOriginalSchemaUriSegment =
postRetrySource.SchemaUriSegment();

resourceGraph.AddVertex(postRetryVertex);
resourceGraph.AddEdge(new AssociationViewEdge(postRetryVertex, postRetryTarget, null));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public static bool IsDerivedFrom(
public static string SchemaUriSegment(this Resource resource) => resource
.ResourceModel?.SchemaNameMapProvider
?.GetSchemaMapByPhysicalName(resource.FullName.Schema)
.UriSegment;
.UriSegment ?? resource.PostRetryOriginalSchemaUriSegment;

/// <summary>
/// Check if resource is abstract.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ public interface IResourceLoadGraphFactory
/// Creates a new graph containing the Ed-Fi model's resources, performing any defined graph transformations.
/// </summary>
/// <returns>A new graph instance.</returns>
BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph();
BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph(bool includePostRetryNodes = true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public ResourceLoadGraphFactory(IResourceModelProvider resourceModelProvider,
_graphTransformers = graphTransformers;
}

public BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph()
public BidirectionalGraph<Resource.Resource, AssociationViewEdge> CreateResourceLoadGraph(bool includePostRetryNodes)
{
var resourceModel = _resourceModelProvider.GetResourceModel();

Expand Down Expand Up @@ -71,9 +71,29 @@ public ResourceLoadGraphFactory(IResourceModelProvider resourceModelProvider,
}
}

resourceGraph.BreakCycles(edge => edge.AssociationView.IsSoftDependency);

resourceGraph.BreakCycles(edge => edge.AssociationView?.IsSoftDependency ?? false);

if (!includePostRetryNodes)
{
RemovePostRetryNodes();
}

return resourceGraph;

void RemovePostRetryNodes()
{
var postRetryVertices = resourceGraph.Vertices.Where(v => v.IsPostRetryResource).ToList();

foreach(var postRetryVertex in postRetryVertices)
{
var outEdges = resourceGraph.OutEdges(postRetryVertex).ToList();
foreach (var edge in outEdges)
{
resourceGraph.RemoveEdge(edge);
}
resourceGraph.RemoveVertex(postRetryVertex);
}
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions Application/EdFi.Ods.Common/Models/Resource/Resource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public Resource(string name)
}

public bool IsEdFiCore { get; set; }

public bool IsPostRetryResource { get; set; }

public string PostRetryOriginalSchemaUriSegment { get; set; }

/// <summary>
/// Gets the root <see cref="Resource" /> class for the current resource.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class AggregateDependencyController : ControllerBase

private readonly ILog _logger = LogManager.GetLogger(typeof(AggregateDependencyController));
private readonly bool _isEnabled;

private const string PostRetrySuffix = "/#POSTRetry";

public AggregateDependencyController(
ApiSettings apiSettings,
Expand All @@ -57,12 +59,16 @@ public IActionResult Get()

try
{
var groupedLoadOrder = GetGroupedLoadOrder(_resourceLoadGraphFactory.CreateResourceLoadGraph()).ToList();
if(Request.GetTypedHeaders().Accept != null
&& Request.GetTypedHeaders().Accept.Any(a =>
a.MediaType.Value.EqualsIgnoreCase(CustomMediaContentTypes.GraphML)))
{
return Ok(CreateGraphML(_resourceLoadGraphFactory.CreateResourceLoadGraph(true)));
}

var groupedLoadOrder = GetGroupedLoadOrder(_resourceLoadGraphFactory.CreateResourceLoadGraph(false)).ToList();
ModifyLoadOrderForAuthorizationConcerns(groupedLoadOrder);
return Request.GetTypedHeaders().Accept != null
&& Request.GetTypedHeaders().Accept.Any(a => a.MediaType.Value.EqualsIgnoreCase(CustomMediaContentTypes.GraphML))
? Ok(CreateGraphML(_resourceLoadGraphFactory.CreateResourceLoadGraph()))
: Ok(groupedLoadOrder);
return Ok(groupedLoadOrder);
}
catch (NonAcyclicGraphException e)
{
Expand Down Expand Up @@ -135,7 +141,12 @@ List<Resource> GetLoadableResources()
}

private static string GetNodeId(Resource resource)
=> $"/{resource.SchemaUriSegment()}/{resource.PluralName.ToCamelCase()}";
{
var suffixToApply = resource.IsPostRetryResource
? PostRetrySuffix
: string.Empty;
return $"/{resource.SchemaUriSegment() ?? resource.PostRetryOriginalSchemaUriSegment}/{resource.PluralName.ToCamelCase()}{suffixToApply}";
}

private static void ModifyLoadOrderForAuthorizationConcerns(IList<ResourceLoadOrder> resources)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
namespace EdFi.Ods.Tests.EdFi.Ods.Features.Controllers
{
[TestFixture]
public class AggregateDependencyControllerTests
public class AggregateDependencyControllerGraphMLTests
{
public class When_getting_the_dependencies_for_loading_data : TestFixtureBase
{
Expand All @@ -40,10 +40,10 @@ protected override void Arrange()
var graph = new BidirectionalGraph<Resource, AssociationViewEdge>();
graph.AddVertex(new Resource("Test"));

A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph())
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph(true))
.Returns(graph);

_controller = CreateController(_resourceLoadGraphFactory);
_controller = CreateController(_resourceLoadGraphFactory, true);
}

protected override void Act()
Expand All @@ -55,7 +55,7 @@ protected override void Act()
[Test]
public void Should_get_the_resource_model_for_building_the_output()
{
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph())
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph(true))
.MustHaveHappened();
}

Expand Down Expand Up @@ -88,7 +88,7 @@ protected override void Arrange()

var graph = new BidirectionalGraph<Resource, AssociationViewEdge>();
graph.AddVertex(new Resource("Test"));
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph())
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph(true))
.Returns(graph);

_controller = CreateController(_resourceLoadGraphFactory, true);
Expand All @@ -104,7 +104,7 @@ protected override void Act()
[Test]
public void Should_call_the_resource_model_provider_to_get_the_model_for_building_the_output()
{
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph()).MustHaveHappened();
A.CallTo(() => _resourceLoadGraphFactory.CreateResourceLoadGraph(true)).MustHaveHappened();
}

[Test]
Expand Down
Loading

0 comments on commit 3b1708c

Please sign in to comment.