Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/Service.Tests/SqlTests/RestApiTests/Find/FindApiTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,22 @@ await SetupAndRunRestApiTest(
);
}

/// <summary>
/// Tests the REST Api for Find operation for all records.
/// Uses entity with mapped columns, and order by title in ascending order.
/// </summary>
[TestMethod]
public async Task FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc()
{
await SetupAndRunRestApiTest(
primaryKeyRoute: string.Empty,
queryString: "?$orderby=fancyName",
entity: _integrationMappingDifferentEntity,
sqlQuery: GetQuery(nameof(FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc)),
controller: _restController
);
}

/// <summary>
/// Tests the REST Api for Find operation for all records
/// when there is a space in the column name.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ public class MsSqlFindApiTests : FindApiTestBase
$"ORDER BY title, id " +
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
},
{
"FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
$"SELECT [treeId], [species] AS [fancyName], [region], [height] FROM { _integrationMappingTable } " +
$"ORDER BY species " +
$"FOR JSON PATH, INCLUDE_NULL_VALUES"
},
{
"FindTestWithQueryStringSpaceInNamesOrderByAsc",
$"SELECT * FROM { _integrationTableHasColumnWithSpace } " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,17 @@ SELECT JSON_ARRAYAGG(JSON_OBJECT('id', id, 'title', title, 'publisher_id', publi
LIMIT 100
) AS subq"
},
{
"FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
@"
SELECT JSON_ARRAYAGG(JSON_OBJECT('treeId', treeId, 'fancyName', species, 'region', region, 'height', height)) AS data
FROM (
SELECT *
FROM " + _integrationMappingTable + @"
ORDER BY species
LIMIT 100
) AS subq"
},
{
"FindTestWithQueryStringSpaceInNamesOrderByAsc",
@"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,16 @@ SELECT json_agg(to_jsonb(subq)) AS data
ORDER BY title, id
) AS subq"
},
{
"FindTestWithQueryStringAllFieldsMappedEntityOrderByAsc",
@"
SELECT json_agg(to_jsonb(subq)) AS data
FROM (
SELECT ""treeId"", ""species"" AS ""fancyName"", ""region"", ""height""
FROM " + _integrationMappingTable + @"
ORDER BY ""species""
) AS subq"
},
{
"FindTestWithFirstAndSpacedColumnOrderBy",
@"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ protected RestRequestContext(string entityName, DatabaseObject dbo)
/// </summary>
public virtual List<OrderByColumn>? OrderByClauseInUrl { get; set; }

/// <summary>
/// List of OrderBy Columns which represent the OrderByClause using backing columns.
/// Based on the operation type, this property may or may not be populated.
/// </summary>
public virtual List<OrderByColumn>? OrderByClauseOfBackingColumns { get; set; }

/// <summary>
/// Dictionary of field names and their values given in the request body.
/// Based on the operation type, this property may or may not be populated.
Expand Down
48 changes: 29 additions & 19 deletions src/Service/Parsers/RequestParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
break;
case SORT_URL:
string sortQueryString = $"?{SORT_URL}={context.ParsedQueryString[key]}";
context.OrderByClauseInUrl = GenerateOrderByList(context, sqlMetadataProvider, sortQueryString);
(context.OrderByClauseInUrl, context.OrderByClauseOfBackingColumns) = GenerateOrderByLists(context, sqlMetadataProvider, sortQueryString);
break;
case AFTER_URL:
context.After = context.ParsedQueryString[key];
Expand All @@ -135,9 +135,9 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
/// associated with the sort param.</param>
/// <returns>A List<OrderByColumns></returns>
/// <exception cref="DataApiBuilderException"></exception>
private static List<OrderByColumn>? GenerateOrderByList(RestRequestContext context,
ISqlMetadataProvider sqlMetadataProvider,
string sortQueryString)
private static (List<OrderByColumn>?, List<OrderByColumn>?) GenerateOrderByLists(RestRequestContext context,
ISqlMetadataProvider sqlMetadataProvider,
string sortQueryString)
{
string schemaName = context.DatabaseObject.SchemaName;
string tableName = context.DatabaseObject.Name;
Expand All @@ -148,7 +148,9 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
// used for performant Remove operations
HashSet<string> remainingKeys = new(primaryKeys);

List<OrderByColumn> orderByList = new();
List<OrderByColumn> orderByListUrl = new();
List<OrderByColumn> orderByListBackingColumn = new();

// OrderBy AST is in the form of a linked list
// so we traverse by calling node.ThenBy until
// node is null
Expand All @@ -159,43 +161,49 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
// column name of null. ie: $orderby='hello world', or $orderby=null
// note: null support is not currently implemented.
QueryNode? expression = node.Expression is not null ? node.Expression :
throw new DataApiBuilderException(message: "OrderBy property is not supported.",
HttpStatusCode.BadRequest,
DataApiBuilderException.SubStatusCodes.BadRequest);
throw new DataApiBuilderException(
message: "OrderBy property is not supported.",
HttpStatusCode.BadRequest,
DataApiBuilderException.SubStatusCodes.BadRequest);

string backingColumnName;
string? backingColumnName;
string exposedName;
if (expression.Kind is QueryNodeKind.SingleValuePropertyAccess)
{
exposedName = ((SingleValuePropertyAccessNode)expression).Property.Name;
sqlMetadataProvider.TryGetBackingColumn(context.EntityName, exposedName, out backingColumnName);
// if name is in SingleValuePropertyAccess node it matches our model and we will
// always be able to get backing column successfully
sqlMetadataProvider.TryGetBackingColumn(context.EntityName, ((SingleValuePropertyAccessNode)expression).Property.Name, out backingColumnName!);
}
else if (expression.Kind is QueryNodeKind.Constant &&
((ConstantNode)expression).Value is not null)
{
// since this comes from constant node, it was not checked against our model
// so this may return false in which case we throw for a bad request
if (!sqlMetadataProvider.TryGetBackingColumn(context.EntityName, ((ConstantNode)expression).Value.ToString()!, out backingColumnName!))
exposedName = ((ConstantNode)expression).Value.ToString()!;
Comment thread
aaronburtle marked this conversation as resolved.
if (!sqlMetadataProvider.TryGetBackingColumn(context.EntityName, exposedName, out backingColumnName))
{
throw new DataApiBuilderException(
message: $"Invalid orderby column requested: {((ConstantNode)expression).Value.ToString()!}.",
message: $"Invalid orderby column requested: {exposedName}.",
statusCode: HttpStatusCode.BadRequest,
subStatusCode: DataApiBuilderException.SubStatusCodes.BadRequest);
}
}
else
{
throw new DataApiBuilderException(message: "OrderBy property is not supported.",
HttpStatusCode.BadRequest,
DataApiBuilderException.SubStatusCodes.BadRequest);
throw new DataApiBuilderException(
message: "OrderBy property is not supported.",
HttpStatusCode.BadRequest,
DataApiBuilderException.SubStatusCodes.BadRequest);
}

// Sorting order is stored in node.Direction as OrderByDirection Enum
// We convert to an Enum of our own that matches the SQL text we want
OrderBy direction = GetDirection(node.Direction);
// Add OrderByColumn and remove any matching columns from our primary key set
orderByList.Add(new OrderByColumn(schemaName, tableName, backingColumnName, direction: direction));
remainingKeys.Remove(backingColumnName);
orderByListUrl.Add(new OrderByColumn(schemaName, tableName, exposedName, direction: direction));
orderByListBackingColumn.Add(new OrderByColumn(schemaName, tableName, backingColumnName!, direction: direction));
remainingKeys.Remove(backingColumnName!);
node = node.ThenBy;
}

Expand All @@ -206,11 +214,13 @@ public static void ParseQueryString(RestRequestContext context, ISqlMetadataProv
{
if (remainingKeys.Contains(column))
{
orderByList.Add(new OrderByColumn(schemaName, tableName, column));
sqlMetadataProvider.TryGetExposedColumnName(context.EntityName, column, out string? exposedName);
Comment thread
aaronburtle marked this conversation as resolved.
orderByListUrl.Add(new OrderByColumn(schemaName, tableName, exposedName!));
orderByListBackingColumn.Add(new OrderByColumn(schemaName, tableName, column));
}
}

return orderByList;
return (orderByListUrl, orderByListBackingColumn);
}

/// <summary>
Expand Down
52 changes: 27 additions & 25 deletions src/Service/Resolvers/Sql Query Structures/SqlQueryStructure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,10 +171,12 @@ public SqlQueryStructure(
value: predicate.Value);
}

// context.OrderByColumnsInUrl will lack TableAlias because it is created in RequestParser
// which may be called for any type of operation. To avoid coupling the OrderByClauseInUrl
// context.OrderByClauseOfBackingColumns will lack TableAlias because it is created in RequestParser
// which may be called for any type of operation. To avoid coupling the OrderByClauseOfBackingColumns
// to only Find, we populate the TableAlias in this constructor where we know we have a Find operation.
OrderByColumns = context.OrderByClauseInUrl is not null ? context.OrderByClauseInUrl : PrimaryKeyAsOrderByColumns();
OrderByColumns = context.OrderByClauseOfBackingColumns is not null ?
context.OrderByClauseOfBackingColumns : PrimaryKeyAsOrderByColumns();

foreach (OrderByColumn column in OrderByColumns)
{
if (string.IsNullOrEmpty(column.TableAlias))
Expand Down Expand Up @@ -247,6 +249,28 @@ private void AddFields(RestRequestContext context, ISqlMetadataProvider sqlMetad
}
}

/// <summary>
/// Exposes the primary key of the underlying table of the structure
/// as a list of OrderByColumn
/// </summary>
private List<OrderByColumn> PrimaryKeyAsOrderByColumns()
{
if (_primaryKeyAsOrderByColumns is null)
{
_primaryKeyAsOrderByColumns = new();

foreach (string column in PrimaryKey())
{
_primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName,
tableName: DatabaseObject.Name,
columnName: column,
tableAlias: TableAlias));
}
}

return _primaryKeyAsOrderByColumns;
}

/// <summary>
/// Private constructor that is used for recursive query generation,
/// for each subquery that's necessary to resolve a nested GraphQL
Expand Down Expand Up @@ -851,28 +875,6 @@ private List<OrderByColumn> ProcessGqlOrderByArg(List<ObjectFieldNode> orderByFi
return orderByColumnsList;
}

/// <summary>
/// Exposes the primary key of the underlying table of the structure
/// as a list of OrderByColumn
/// </summary>
public List<OrderByColumn> PrimaryKeyAsOrderByColumns()
Comment thread
seantleonard marked this conversation as resolved.
{
if (_primaryKeyAsOrderByColumns == null)
{
_primaryKeyAsOrderByColumns = new();

foreach (string column in PrimaryKey())
{
_primaryKeyAsOrderByColumns.Add(new OrderByColumn(tableSchema: DatabaseObject.SchemaName,
tableName: DatabaseObject.Name,
columnName: column,
tableAlias: TableAlias));
}
}

return _primaryKeyAsOrderByColumns;
}

/// <summary>
/// Adds a labelled column to this query's columns, where
/// the column name is all that is provided, and we add
Expand Down
2 changes: 1 addition & 1 deletion src/Service/Resolvers/SqlQueryEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ private OkObjectResult FormatFindResult(JsonDocument jsonDoc, FindRequestContext
rootEnumerated = rootEnumerated.Take(rootEnumerated.Count() - 1);
string after = SqlPaginationUtil.MakeCursorFromJsonElement(
element: rootEnumerated.Last(),
orderByColumns: context.OrderByClauseInUrl,
orderByColumns: context.OrderByClauseOfBackingColumns,
primaryKey: _sqlMetadataProvider.GetTableDefinition(context.EntityName).PrimaryKey,
entityName: context.EntityName,
schemaName: context.DatabaseObject.SchemaName,
Expand Down