Skip to content

Commit

Permalink
[ODS-6432] Add support for key set paging (#1143)
Browse files Browse the repository at this point in the history
  • Loading branch information
gmcelhanon authored Sep 26, 2024
1 parent c3324e0 commit a5a7084
Show file tree
Hide file tree
Showing 81 changed files with 6,106 additions and 1,481 deletions.
1 change: 1 addition & 0 deletions Application/EdFi.Ods.Api/Constants/HeaderConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ public class HeaderConstants
public const string XForwardedHost = "X-Forwarded-Host";
public const string XForwardedPort = "X-Forwarded-Port";
public const string ContentType = "Content-Type";
public const string NextPageToken = "Next-Page-Token";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
using EdFi.Ods.Api.Configuration;
using EdFi.Ods.Api.Conventions;
using EdFi.Ods.Api.ExceptionHandling;
using EdFi.Ods.Api.ExternalTasks;
using EdFi.Ods.Api.Filters;
using EdFi.Ods.Api.IdentityValueMappers;
using EdFi.Ods.Api.Infrastructure.Pipelines.Factories;
Expand All @@ -27,6 +26,7 @@
using EdFi.Ods.Api.Providers;
using EdFi.Ods.Api.Security.Authentication;
using EdFi.Ods.Api.Serialization;
using EdFi.Ods.Api.Startup;
using EdFi.Ods.Api.Validation;
using EdFi.Ods.Common;
using EdFi.Ods.Common.Caching;
Expand Down Expand Up @@ -377,7 +377,7 @@ protected override void Load(ContainerBuilder builder)
.SingleInstance();

builder.RegisterType<InitializeScheduledJobs>()
.As<IExternalTask>();
.As<IStartupCommand>();

// Register components for string encryption/decryption
builder.RegisterType<Aes256SymmetricStringEncryptionProvider>()
Expand Down
27 changes: 27 additions & 0 deletions Application/EdFi.Ods.Api/Container/Modules/KeySetPagingModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using Autofac;
using Autofac.Features.AttributeFilters;
using EdFi.Ods.Api.Controllers.Partitions.Controllers;
using EdFi.Ods.Common.Providers.Queries;

namespace EdFi.Ods.Api.Container.Modules;

public class KeySetPagingModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<PartitionsQueryBuilderProvider>()
.As<IPartitionsQueryBuilderProvider>()
.SingleInstance()
.WithAttributeFiltering();

builder.RegisterType<PartitionRowNumbersCteQueryBuilderProvider>()
.Keyed<IAggregateRootQueryBuilderProvider>(PartitionRowNumbersCteQueryBuilderProvider.RegistrationKey)
.AsSelf()
.SingleInstance();
}
}
28 changes: 25 additions & 3 deletions Application/EdFi.Ods.Api/Container/Modules/PersistenceModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Autofac;
using Autofac.Core;
using Autofac.Extras.DynamicProxy;
using Autofac.Features.AttributeFilters;
using EdFi.Admin.DataAccess.Providers;
using EdFi.Common.Database;
using EdFi.Common.Extensions;
Expand All @@ -24,12 +25,15 @@
using EdFi.Ods.Common.Infrastructure.Configuration;
using EdFi.Ods.Common.Infrastructure.Repositories;
using EdFi.Ods.Common.Providers;
using EdFi.Ods.Common.Providers.Criteria;
using EdFi.Ods.Common.Providers.Queries;
using EdFi.Ods.Common.Providers.Queries.Criteria;
using EdFi.Ods.Common.Providers.Queries.Paging;
using EdFi.Ods.Common.Repositories;
using EdFi.Security.DataAccess.Providers;
using Microsoft.Extensions.Caching.Memory;
using NHibernate;
using IInterceptor = Castle.DynamicProxy.IInterceptor;
using Module = Autofac.Module;

namespace EdFi.Ods.Api.Container.Modules
{
Expand Down Expand Up @@ -101,10 +105,27 @@ protected override void Load(ContainerBuilder builder)
.As<IOdsDatabaseAccessIntentProvider>()
.SingleInstance();

builder.RegisterGeneric(typeof(PagedAggregateIdsCriteriaProvider<>))
.As(typeof(IPagedAggregateIdsCriteriaProvider<>))
// Paged query builder
builder.RegisterType<PagedAggregateIdsQueryBuilderProvider>()
.Keyed<IAggregateRootQueryBuilderProvider>(PagedAggregateIdsQueryBuilderProvider.RegistrationKey)
.SingleInstance();

// Limit/offset paging support
builder.RegisterType<LimitOffsetPagingStrategy>()
.Keyed<IPagingStrategy>(PagingStrategy.LimitOffset)
.SingleInstance();

// Key set paging support
builder.RegisterType<KeySetPagingStrategy>()
.Keyed<IPagingStrategy>(PagingStrategy.KeySet)
.SingleInstance();

// Additional criteria applicators
builder.RegisterAssemblyTypes(typeof(IAggregateRootQueryCriteriaApplicator).Assembly)
.Where(t => t.IsImplementationOf<IAggregateRootQueryCriteriaApplicator>())
.As<IAggregateRootQueryCriteriaApplicator>();

// Repository operations
builder.RegisterGeneric(typeof(CreateEntity<>))
.As(typeof(ICreateEntity<>))
.SingleInstance();
Expand All @@ -123,6 +144,7 @@ protected override void Load(ContainerBuilder builder)

builder.RegisterGeneric(typeof(GetEntitiesBySpecification<>))
.As(typeof(IGetEntitiesBySpecification<>))
.WithAttributeFiltering()
.SingleInstance();

builder.RegisterGeneric(typeof(GetEntityById<>))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers;
using Polly;
using Polly.Retry;

namespace EdFi.Ods.Api.Controllers
{
Expand Down Expand Up @@ -146,22 +144,23 @@ private string GetReadContentType()
[ServiceFilter(typeof(EnforceAssignedProfileUsageFilter), IsReusable = true)]
public virtual async Task<IActionResult> GetAll(
[FromQuery] UrlQueryParametersRequest urlQueryParametersRequest,
[FromQuery] TGetByExampleRequest request = default(TGetByExampleRequest))
[FromQuery] TGetByExampleRequest request = default,
[FromQuery] Dictionary<string, string> additionalParameters = default)
{
//respond quickly to DOS style requests (should we catch these earlier? e.g. attribute filter?)
if (urlQueryParametersRequest.Limit != null &&
(urlQueryParametersRequest.Limit < 0 || urlQueryParametersRequest.Limit > _defaultPageLimitSize))

var queryParameters = new QueryParameters(urlQueryParametersRequest);

if (!QueryParametersValidator.IsValid(queryParameters, _defaultPageLimitSize, out string errorMessage))
{
var problemDetails = new BadRequestParameterException(
"The limit parameter was incorrect.",
new[] { $"Limit must be omitted or set to a value between 0 and {_defaultPageLimitSize}." })
var problemDetails = new BadRequestParameterException(BadRequestException.DefaultDetail, [errorMessage])
{
CorrelationId = _logContextAccessor.GetCorrelationId()
}.AsSerializableModel();

return BadRequest(problemDetails);
}

var internalRequestAsResource = new TResourceModel();
var internalRequest = internalRequestAsResource as TEntityInterface;

Expand All @@ -170,12 +169,10 @@ public virtual async Task<IActionResult> GetAll(
MapAll(request, internalRequest);
}

var queryParameters = new QueryParameters(urlQueryParametersRequest);

// Execute the pipeline (synchronously)
var result = await GetManyPipeline.Value
.ProcessAsync(
new GetManyContext<TResourceModel, TAggregateRoot>(internalRequestAsResource, queryParameters),
new GetManyContext<TResourceModel, TAggregateRoot>(internalRequestAsResource, queryParameters, additionalParameters),
new CancellationToken());

// Handle exception result
Expand All @@ -191,6 +188,11 @@ public virtual async Task<IActionResult> GetAll(
Response.Headers.Append(HeaderConstants.TotalCount, result.ResultMetadata.TotalCount.ToString());
}

if (queryParameters.MinAggregateId != null && result.Resources.Count > 0)
{
Response.Headers.Append(HeaderConstants.NextPageToken, result.ResultMetadata.NextPageToken);
}

Response.GetTypedHeaders().ContentType = new MediaTypeHeaderValue(GetReadContentType());
return Ok(result.Resources);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System.Collections.Generic;
using EdFi.Ods.Common.Database.Querying;
using EdFi.Ods.Common.Models.Domain;

namespace EdFi.Ods.Api.Controllers.Partitions.Controllers;

/// <summary>
/// Defines the interface for obtaining the <see cref="QueryBuilder" /> for the main Partitions query.
/// </summary>
public interface IPartitionsQueryBuilderProvider
{
/// <summary>
/// Get the <see cref="QueryBuilder" /> for the main Partitions query.
/// </summary>
/// <param name="numberOfPartitions"></param>
/// <param name="aggregateRootEntity"></param>
/// <param name="specification"></param>
/// <param name="additionalParameters"></param>
/// <returns></returns>
QueryBuilder GetQueryBuilder(
int numberOfPartitions,
Entity aggregateRootEntity,
AggregateRootWithCompositeKey specification,
IDictionary<string, string> additionalParameters);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
// SPDX-License-Identifier: Apache-2.0
// Licensed to the Ed-Fi Alliance under one or more agreements.
// The Ed-Fi Alliance licenses this file to you under the Apache License, Version 2.0.
// See the LICENSE and NOTICES files in the project root for more information.

using System;
using System.Collections.Generic;
using EdFi.Common.Configuration;
using EdFi.Ods.Api.Security.Authorization;
using EdFi.Ods.Api.Security.Authorization.Filtering;
using EdFi.Ods.Common;
using EdFi.Ods.Common.Context;
using EdFi.Ods.Common.Database.Querying;
using EdFi.Ods.Common.Database.Querying.Dialects;
using EdFi.Ods.Common.Models;
using EdFi.Ods.Common.Models.Domain;
using EdFi.Ods.Common.Providers.Queries;
using EdFi.Ods.Common.Providers.Queries.Criteria;
using EdFi.Ods.Common.Security;
using EdFi.Ods.Common.Security.Authorization;
using EdFi.Ods.Common.Security.Claims;
using EdFi.Security.DataAccess.Repositories;

namespace EdFi.Ods.Api.Controllers.Partitions.Controllers;

public class PartitionRowNumbersCteQueryBuilderProvider : IAggregateRootQueryBuilderProvider
{
public const string RegistrationKey = "PartitionRowNumbersCte";

private readonly Dialect _dialect;
private readonly DatabaseEngine _databaseEngine;
private readonly IAggregateRootQueryCriteriaApplicator[] _additionalParametersCriteriaApplicator;

// Security dependencies
private readonly IContextProvider<DataManagementResourceContext> _dataManagementResourceContextProvider;
private readonly IApiClientContextProvider _apiClientContextProvider;
private readonly IAuthorizationContextProvider _authorizationContextProvider;
private readonly IAuthorizationBasisMetadataSelector _authorizationBasisMetadataSelector;
private readonly IAuthorizationFilteringProvider _authorizationFilteringProvider;
private readonly IAuthorizationFilterContextProvider _authorizationFilterContextProvider;
private readonly ISecurityRepository _securityRepository;
private readonly IResourceClaimUriProvider _resourceClaimUriProvider;

public PartitionRowNumbersCteQueryBuilderProvider(
Dialect dialect,
DatabaseEngine databaseEngine,
IAggregateRootQueryCriteriaApplicator[] additionalParametersCriteriaApplicator,

// Security dependencies
IApiClientContextProvider apiClientContextProvider,
IContextProvider<DataManagementResourceContext> dataManagementResourceContextProvider,
IAuthorizationContextProvider authorizationContextProvider,
IAuthorizationBasisMetadataSelector authorizationBasisMetadataSelector,
IAuthorizationFilteringProvider authorizationFilteringProvider,
IAuthorizationFilterContextProvider authorizationFilterContextProvider,
ISecurityRepository securityRepository,
IResourceClaimUriProvider resourceClaimUriProvider)
{
_dialect = dialect;
_databaseEngine = databaseEngine;
_additionalParametersCriteriaApplicator = additionalParametersCriteriaApplicator;
_apiClientContextProvider = apiClientContextProvider;
_dataManagementResourceContextProvider = dataManagementResourceContextProvider;
_authorizationContextProvider = authorizationContextProvider;
_authorizationBasisMetadataSelector = authorizationBasisMetadataSelector;
_authorizationFilteringProvider = authorizationFilteringProvider;
_authorizationFilterContextProvider = authorizationFilterContextProvider;
_securityRepository = securityRepository;
_resourceClaimUriProvider = resourceClaimUriProvider;
}

public QueryBuilder GetQueryBuilder(
Entity entity,
AggregateRootWithCompositeKey specification,
IQueryParameters queryParameters,
IDictionary<string, string> additionalParameters)
{
// TODO: ODS-6510 - This needs to be invokes an authorization decorator of some sort -- copied from the data management controller pipeline. Also, look for approach to DRY here.
EstablishAuthorizationFilteringContext(entity);

var rowNumbersQueryBuilder = new QueryBuilder(_dialect);

// Get the fully qualified physical table name
var schemaTableName = $"{entity.Schema}.{entity.TableName(_databaseEngine)}";

string rootTableAlias = entity.IsDerived ? "b" : "r";

rowNumbersQueryBuilder
.From(schemaTableName.Alias("r"))
.Select($"{rootTableAlias}.AggregateId")
.SelectRaw($"ROW_NUMBER() OVER (ORDER BY {rootTableAlias}.AggregateId) AS RowNumber");

// Add the join to the base type
if (entity.IsDerived)
{
rowNumbersQueryBuilder.Join(
$"{entity.BaseEntity.Schema}.{entity.BaseEntity.TableName(_databaseEngine)} AS b",
j =>
{
foreach (var propertyMapping in entity.BaseAssociation.PropertyMappings)
{
j.On(
$"r.{propertyMapping.ThisProperty.ColumnNameByDatabaseEngine[_databaseEngine]}",
$"b.{propertyMapping.OtherProperty.ColumnNameByDatabaseEngine[_databaseEngine]}");
}

return j;
});
}

// Add special query fields
AggregateQueryBuilderHelpers.ProcessCommonQueryParameters(rowNumbersQueryBuilder, queryParameters);

// Apply additional parameters, as applicable
foreach (var applicator in _additionalParametersCriteriaApplicator)
{
applicator.ApplyAdditionalParameters(rowNumbersQueryBuilder, entity, specification, additionalParameters);
}

return rowNumbersQueryBuilder;
}

// TODO: ODS-6510 - THIS NEEDS TO REFACTORED OUT INTO A SECURITY COMPONENT SOMEWHERE - Pay attention to DRY
private void EstablishAuthorizationFilteringContext(dynamic aggregateRootEntity)
{
// Establish the authorization context -- currently done in SetAuthorizationContext pipeline step, not accessible here
_authorizationContextProvider.SetResourceUris(
_resourceClaimUriProvider.GetResourceClaimUris(aggregateRootEntity));

_authorizationContextProvider.SetAction(_securityRepository.GetActionByName("Read").ActionUri);

// Make sure Authorization context is present before proceeding
_authorizationContextProvider.VerifyAuthorizationContextExists();

// Build the AuthorizationContext
var apiClientContext = _apiClientContextProvider.GetApiClientContext();
var resource = _dataManagementResourceContextProvider.Get().Resource;
string[] resourceClaimUris = _authorizationContextProvider.GetResourceUris();
string requestActionUri = _authorizationContextProvider.GetAction();

var authorizationContext = new EdFiAuthorizationContext(
apiClientContext,
resource,
resourceClaimUris,
requestActionUri,
aggregateRootEntity.NHibernateEntityType);

// Get authorization filters
var authorizationBasisMetadata = _authorizationBasisMetadataSelector.SelectAuthorizationBasisMetadata(
apiClientContext.ClaimSetName,
resourceClaimUris,
requestActionUri);

var authorizationFiltering = _authorizationFilteringProvider.GetAuthorizationFiltering(authorizationContext, authorizationBasisMetadata);

_authorizationFilterContextProvider.SetFilterContext(authorizationFiltering);
}
}
Loading

0 comments on commit a5a7084

Please sign in to comment.