in src/Core/Resolvers/SqlPaginationUtil.cs [323:456]
public static IEnumerable<PaginationColumn> ParseAfterFromJsonString(
string afterJsonString,
PaginationMetadata paginationMetadata,
ISqlMetadataProvider sqlMetadataProvider,
string entityName,
RuntimeConfigProvider runtimeConfigProvider
)
{
List<PaginationColumn>? paginationCursorColumnsForQuery = new();
IEnumerable<NextLinkField>? paginationCursorFieldsFromRequest;
try
{
afterJsonString = Base64Decode(afterJsonString);
paginationCursorFieldsFromRequest = JsonSerializer.Deserialize<IEnumerable<NextLinkField>>(afterJsonString);
if (paginationCursorFieldsFromRequest is null)
{
throw new ArgumentException("Failed to parse the pagination information from the provided token");
}
Dictionary<string, PaginationColumn> exposedFieldNameToBackingColumn = new();
foreach (NextLinkField field in paginationCursorFieldsFromRequest)
{
// REST calls this function with a non null sqlMetadataProvider
// which will get the exposed name for safe messaging in the response.
// Since we are looking for pagination columns from the $after query
// param, we expect this column to exist as the $after query param
// was formed from a previous response with a nextLink. If the nextLink
// has been modified and backingColumn is null we throw exception.
string backingColumnName = GetBackingColumnName(entityName, field.FieldName, sqlMetadataProvider);
if (backingColumnName is null)
{
throw new DataApiBuilderException(
message: $"Pagination token is not well formed because {field.FieldName} is not valid.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
PaginationColumn pageColumn = new(
tableName: "",
tableSchema: "",
columnName: backingColumnName,
value: field.FieldValue,
paramName: field.ParamName,
direction: field.Direction);
paginationCursorColumnsForQuery.Add(pageColumn);
// holds exposed name mapped to exposed pagination column
exposedFieldNameToBackingColumn.Add(field.FieldName, pageColumn);
}
// verify that primary keys is a sub set of after's column names
// if any primary keys are not contained in after's column names we throw exception
List<string> primaryKeys = paginationMetadata.Structure!.PrimaryKey();
if (!paginationMetadata.RequestedGroupBy)
{
// primary key not valid check for groupby ordering.
foreach (string pk in primaryKeys)
{
// REST calls this function with a non null sqlMetadataProvider
// which will get the exposed name for safe messaging in the response.
// Since we are looking for primary keys we expect these columns to
// exist.
string exposedFieldName = GetExposedColumnName(entityName, pk, sqlMetadataProvider);
if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName))
{
throw new DataApiBuilderException(
message: $"Pagination token is not well formed because it is missing an expected field: {exposedFieldName}",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
}
// verify that orderby columns for the structure and the after columns
// match in name and direction
int orderByColumnCount = 0;
SqlQueryStructure structure = paginationMetadata.Structure!;
foreach (OrderByColumn column in structure.OrderByColumns)
{
string exposedFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
if (!exposedFieldNameToBackingColumn.ContainsKey(exposedFieldName) ||
exposedFieldNameToBackingColumn[exposedFieldName].Direction != column.Direction)
{
// REST calls this function with a non null sqlMetadataProvider
// which will get the exposed name for safe messaging in the response.
// Since we are looking for valid orderby columns we expect
// these columns to exist.
string exposedOrderByFieldName = GetExposedColumnName(entityName, column.ColumnName, sqlMetadataProvider);
throw new DataApiBuilderException(
message: $"Could not match order by column {exposedOrderByFieldName} with a column in the pagination token with the same name and direction.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
orderByColumnCount++;
}
// the check above validates that all orderby columns are matched with after columns
// also validate that there are no extra after columns
if (exposedFieldNameToBackingColumn.Count != orderByColumnCount)
{
throw new ArgumentException("After token contains extra columns not present in order by columns.");
}
}
catch (Exception e) when (
e is InvalidCastException ||
e is ArgumentException ||
e is ArgumentNullException ||
e is FormatException ||
e is System.Text.DecoderFallbackException ||
e is JsonException ||
e is NotSupportedException
)
{
// Possible sources of exceptions:
// stringObject cannot be converted to string
// afterPlainText cannot be successfully decoded
// afterJsonString cannot be deserialized
// keys of afterDeserialized do not correspond to the primary key
// values given for the primary keys are of incorrect format
// duplicate column names in the after token and / or the orderby columns
string errorMessage = runtimeConfigProvider.GetConfig().IsDevelopmentMode() ? $"{e.Message}\n{e.StackTrace}" :
$"{afterJsonString} is not a valid pagination token.";
throw new DataApiBuilderException(
message: errorMessage,
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest,
innerException: e);
}
return paginationCursorColumnsForQuery;
}