Skip to content

Commit

Permalink
[ODS-6545] Align API's paging behavior with recent changes to Open AP…
Browse files Browse the repository at this point in the history
…I metadata (#1178)
  • Loading branch information
gmcelhanon authored Nov 14, 2024
1 parent 35d01c4 commit 6eaf2f9
Show file tree
Hide file tree
Showing 7 changed files with 421 additions and 96 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ public virtual async Task<IActionResult> GetAll(
Response.Headers.Append(HeaderConstants.TotalCount, result.ResultMetadata.TotalCount.ToString());
}

if (queryParameters.MinAggregateId != null && result.Resources.Count > 0)
if (result.Resources.Count > 0)
{
Response.Headers.Append(HeaderConstants.NextPageToken, result.ResultMetadata.NextPageToken);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
// 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.
Expand All @@ -25,8 +25,7 @@ public class GetEntitiesBySpecification<TEntity>
: NHibernateRepositoryOperationBase, IGetEntitiesBySpecification<TEntity>
where TEntity : AggregateRootWithCompositeKey
{
public const string _Id = "Id";
private static IList<TEntity> EmptyList = new List<TEntity>();
private static readonly IList<TEntity> _emptyList = Array.Empty<TEntity>();

private readonly IAggregateRootQueryBuilderProvider _pagedAggregateIdsCriteriaProvider;
private readonly IDomainModelProvider _domainModelProvider;
Expand All @@ -52,7 +51,7 @@ public async Task<GetBySpecificationResult<TEntity>> GetBySpecificationAsync(
CancellationToken cancellationToken)
{
var entityFullName = specification.GetApiModelFullName();

if (!_domainModelProvider.GetDomainModel().EntityByFullName.TryGetValue(entityFullName, out var aggregateRootEntity))
{
throw new Exception($"Unable to find API model entity for '{entityFullName}'.");
Expand All @@ -69,7 +68,7 @@ public async Task<GetBySpecificationResult<TEntity>> GetBySpecificationAsync(
{
return new GetBySpecificationResult<TEntity>
{
Results = EmptyList,
Results = _emptyList,
ResultMetadata = new ResultMetadata
{
TotalCount = specificationResult.TotalCount,
Expand All @@ -83,14 +82,9 @@ public async Task<GetBySpecificationResult<TEntity>> GetBySpecificationAsync(

var result = await _getEntitiesByAggregateIds.GetByAggregateIdsAsync(aggregateIds, cancellationToken);

string nextPageToken = null;

if (queryParameters.MinAggregateId != null)
{
nextPageToken = PagingHelpers.GetPageToken(
specificationResult.Ids[^1].AggregateId + 1,
queryParameters.MaxAggregateId!.Value);
}
string nextPageToken = PagingHelpers.GetPageToken(
specificationResult.Ids[^1].AggregateId + 1,
queryParameters.MaxAggregateId ?? int.MaxValue);

return new GetBySpecificationResult<TEntity>
{
Expand Down Expand Up @@ -133,7 +127,7 @@ async Task<SpecificationResult> GetPagedAggregateIdsAsync()
{
if (countTemplateParameters.ParameterNames.Contains("MinAggregateId"))
{
throw new BadRequestParameterException(BadRequestException.DefaultDetail, ["Total count cannot be determined while using key set paging."]);
throw new BadRequestParameterException(BadRequestException.DefaultDetail, ["Total count cannot be determined while using cursor paging (when pageToken is specified)."]);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,85 @@
// 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 EdFi.Ods.Common.Providers.Queries.Paging;

namespace EdFi.Ods.Common.Models.Queries;

public class QueryParametersValidator
public static class QueryParametersValidator
{
public static bool IsValid(QueryParameters queryParameters, int defaultPageLimitSize, out string errorMessage)
public static bool IsValid(IQueryParameters queryParameters, int defaultPageLimitSize, out string errorMessage)
{
errorMessage = string.Empty;

// Check if offset/limit paging is being used
bool isOffsetLimitPagingProvided = queryParameters.Offset.HasValue || queryParameters.Limit.HasValue;

// Check if key set paging is being used
bool isKeySetPagingProvided = queryParameters.PageSize.HasValue
|| queryParameters.MinAggregateId.HasValue
|| queryParameters.MaxAggregateId.HasValue;
errorMessage = null;

// Ensure that only one type of paging is provided
if (isOffsetLimitPagingProvided && isKeySetPagingProvided)
// Look for multiple paging approaches indicated by parameters
if (queryParameters.Offset.HasValue
&& (queryParameters.MinAggregateId.HasValue || queryParameters.MaxAggregateId.HasValue))
{
errorMessage =
"Both offset/limit and key set paging parameters are provided, but only one type of paging can be used.";
errorMessage = "Both offset and pageToken parameters were provided, but they support alternative paging approaches and cannot be used together.";

return false;
}

// // Validate offset/limit paging: if one parameter is provided, both must be provided
if (isOffsetLimitPagingProvided)
// Determine which paging strategy is in use
var pagingStrategy = queryParameters.MinAggregateId.HasValue || queryParameters.MaxAggregateId.HasValue
? PagingStrategy.KeySet
: PagingStrategy.LimitOffset;

if (pagingStrategy == PagingStrategy.LimitOffset)
{
if ((queryParameters.Offset ?? 0) < 0)
// Validate basic parameter usage
if (queryParameters.PageSize.HasValue && !queryParameters.Limit.HasValue)
{
if (queryParameters.Offset.HasValue)
{
errorMessage = "Use limit instead of pageSize when using limit/offset paging.";
}
else
{
errorMessage = "PageToken is required when pageSize is specified.";
}

return false;
}

// Validate the values provided
if (queryParameters.Offset is < 0)
{
errorMessage = $"Offset cannot be a negative value.";
return false;
}
if ((queryParameters.Limit ?? 0) < 0 || (queryParameters.Limit ?? defaultPageLimitSize) > defaultPageLimitSize)

if (queryParameters.Limit is < 0)
{
errorMessage = $"Limit must be omitted or set to a value between 0 and {defaultPageLimitSize}.";
errorMessage = $"Limit must be a value between 0 and {defaultPageLimitSize}.";
return false;
}
}

// Validate key set paging: if one parameter is provided, all must be provided
if (isKeySetPagingProvided)
if (queryParameters.Limit.HasValue && queryParameters.Limit > defaultPageLimitSize)
{
errorMessage = $"Limit must be a value between 0 and {defaultPageLimitSize}.";
return false;
}
}
else
{
if (!queryParameters.PageSize.HasValue
|| !queryParameters.MinAggregateId.HasValue
|| !queryParameters.MaxAggregateId.HasValue)
// Cursor paging
if (queryParameters.Limit.HasValue && !queryParameters.PageSize.HasValue)
{
errorMessage = "Page token and page size must both be provided for key set paging.";
errorMessage = "Use pageSize instead of limit when using cursor paging with pageToken.";
return false;
}

// Validate the values provided
if (queryParameters.PageSize is < 0)
{
errorMessage = $"PageSize must be a value between 0 and {defaultPageLimitSize}.";
return false;
}

if (queryParameters.PageSize.HasValue && queryParameters.PageSize > defaultPageLimitSize)
{
errorMessage = $"PageSize must be a value between 0 and {defaultPageLimitSize}.";
return false;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public PagingParameters() { }

public PagingParameters(IQueryParameters queryParameters)
{
if (queryParameters.MinAggregateId != null && queryParameters.MaxAggregateId != null && queryParameters.PageSize != null)
if (queryParameters.MinAggregateId != null && queryParameters.MaxAggregateId != null)
{
PageSize = queryParameters.PageSize;
MinAggregateId = queryParameters.MinAggregateId;
Expand Down
Loading

0 comments on commit 6eaf2f9

Please sign in to comment.