server: Data Connectors support for ordering by related table column and aggregates [GDW-126]

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5478
GitOrigin-RevId: 269d33d48f7d41efc7ab4ac6efd9442c6741d08c
This commit is contained in:
Daniel Chambers 2022-08-19 17:00:46 +10:00 committed by hasura-bot
parent 4be1ca758d
commit ef0ca7dea2
36 changed files with 1927 additions and 294 deletions

View File

@ -250,7 +250,7 @@ and here is the resulting query request payload:
"expressions": [],
"type": "and"
},
"order_by": [],
"order_by": null,
"limit": null,
"offset": null,
"fields": {
@ -412,27 +412,6 @@ Here's another example, which corresponds to the predicate "`first_name` is the
}
```
#### Ordering
The `order_by` field specifies an array of zero-or-more _orderings_, each of which consists of a field to order records by, and an order which is either `asc` (ascending) or `desc` (descending).
If there are multiple orderings specified then records should be ordered lexicographically, with earlier orderings taking precedence.
For example, to order records principally by `last_name`, delegating to `first_name` in the case where two last names are equal, we would use the following `order_by` structure:
```json
[
{
"field": "last_name",
"order_type": "asc"
},
{
"field": "first_name",
"order_type": "asc"
}
]
```
#### Relationships
If the call to `GET /capabilities` returns a `capabilities` record with a `relationships` field then the query structure may include fields corresponding to relationships.
@ -483,7 +462,7 @@ This will generate the following JSON query if the agent supports relationships:
"type": "and"
},
"offset": null,
"order_by": [],
"order_by": null,
"limit": null,
"fields": {
"Albums": {
@ -495,7 +474,7 @@ This will generate the following JSON query if the agent supports relationships:
"type": "and"
},
"offset": null,
"order_by": [],
"order_by": null,
"limit": null,
"fields": {
"Title": {
@ -526,7 +505,7 @@ Note the `Albums` field in particular, which traverses the `Artists` -> `Albums`
"type": "and"
},
"offset": null,
"order_by": [],
"order_by": null,
"limit": null,
"fields": {
"Title": {
@ -983,6 +962,176 @@ This would be expected to return the following response, with the rows from the
}
```
#### Ordering
The `order_by` field can either be null, which means no particular ordering is required, or an object with two properties:
```json
{
"relations": {},
"elements": [
{
"target_path": [],
"target": {
"type": "column",
"column": "last_name"
},
"order_direction": "asc"
},
{
"target_path": [],
"target": {
"type": "column",
"column": "first_name"
},
"order_direction": "desc"
}
]
}
```
The `elements` field specifies an array of one-or-more ordering elements. Each element represents a "target" to order, and a direction to order by. The direction can either be `asc` (ascending) or `desc` (descending). If there are multiple elements specified, then rows should be ordered with earlier elements in the array taking precedence. In the above example, rows are principally ordered by `last_name`, delegating to `first_name` in the case where two last names are equal.
The order by element `target` is specified as an object, whose `type` property specifies a different sort of ordering target:
| type | Additional fields | Description |
|------|-------------------|-------------|
| `column` | `column` | Sort by the `column` specified |
| `star_count_aggregate` | - | Sort by the count of all rows on the related target table (a non-empty `target_path` will always be specified) |
| `single_column_aggregate` | `function`, `column` | Sort by the value of applying the specified aggregate function to the column values of the rows in the related target table (a non-empty `target_path` will always be specified) |
The `target_path` property is a list of relationships to navigate before finding the `target` to sort on. This is how sorting on columns or aggregates on related tables is expressed. Note that aggregate-typed targets will never be found on the current table (ie. a `target_path` of `[]`) and are always applied to a related table.
Here's an example of applying an ordering by a related table; the Album table is being queried and sorted by the Album's Artist's Name.
```json
{
"table": ["Album"],
"table_relationships": [
{
"source_table": ["Album"],
"relationships": {
"Artist": {
"target_table": ["Artist"],
"relationship_type": "object",
"column_mapping": {
"ArtistId": "ArtistId"
}
}
}
}
],
"query": {
"fields": {
"Title": { "type": "column", "column": "Title" }
},
"order_by": {
"relations": {
"Artist": {
"where": null,
"subrelations": {}
}
},
"elements": [
{
"target_path": ["Artist"],
"target": {
"type": "column",
"column": "Name"
},
"order_direction": "desc"
}
]
}
}
}
```
Note that the `target_path` specifies the relationship path of `["Artist"]`, and that this relationship is defined in the top-level `table_relationships`. The ordering element target column `Name` would therefore be found on the `Artist` table after joining to it from each `Album`. (See the [Relationships](#Relationships) section for more information about relationships.)
The `relations` property of `order_by` will contain all the relations used in the order by, for the purpose of specifying filters that must be applied to the joined tables before using them for sorting. The `relations` property captures all `target_path`s used in the `order_by` in a recursive fashion, so for example, if the following `target_path`s were used in the `order_by`'s `elements`:
* `["Artist", "Albums"]`
* `["Artist"]`
* `["Tracks"]`
Then the value of the `relations` property would look like this:
```json
{
"Artist": {
"where": null,
"subrelations": {
"Albums": {
"where": null,
"subrelations": {}
}
}
},
"Tracks": {
"where": null,
"subrelations": {}
}
}
```
The `where` properties may contain filtering expressions that must be applied to the joined table before using it for sorting. The filtering expressions are defined in the same manner as specified in the [Filters](#Filters) section of this document, where they are used on the `where` property of Queries.
For example, here's a query that retrieves artists ordered descending by the count of all their albums where the album title is greater than 'T'.
```json
{
"table": ["Artist"],
"table_relationships": [
{
"source_table": ["Artist"],
"relationships": {
"Albums": {
"target_table": ["Album"],
"relationship_type": "array",
"column_mapping": {
"ArtistId": "ArtistId"
}
}
}
}
],
"query": {
"fields": {
"Name": { "type": "column", "column": "Name" }
},
"order_by": {
"relations": {
"Albums": {
"where": {
"type": "binary_op",
"operator": "greater_than",
"column": {
"path": [],
"name": "Title"
},
"value": {
"type": "scalar",
"value": "T"
}
},
"subrelations": {}
}
},
"elements": [
{
"target_path": ["Albums"],
"target": {
"type": "star_count_aggregate"
},
"order_direction": "desc"
}
]
}
}
}
```
#### Type Definitions
The `QueryRequest` TypeScript type in the [reference implementation](./reference/src/types/index.ts) describes the valid request body payloads which may be passed to the `POST /query` endpoint. The response body structure is captured by the `QueryResponse` type.

View File

@ -1,13 +1,21 @@
import { QueryRequest, TableRelationships, Relationship, Query, Field, OrderBy, Expression, BinaryComparisonOperator, UnaryComparisonOperator, BinaryArrayComparisonOperator, ComparisonColumn, ComparisonValue, ScalarValue, QueryResponse, Aggregate, SingleColumnAggregate, ColumnCountAggregate, TableName } from "./types";
import { QueryRequest, TableRelationships, Relationship, Query, Field, OrderBy, Expression, BinaryComparisonOperator, UnaryComparisonOperator, BinaryArrayComparisonOperator, ComparisonColumn, ComparisonValue, ScalarValue, Aggregate, SingleColumnAggregate, ColumnCountAggregate, TableName, OrderByElement, OrderByRelation } from "./types";
import { coerceUndefinedToNull, crossProduct, tableNameEquals, unreachable, zip } from "./util";
import * as math from "mathjs";
type RelationshipName = string
// This is a more constrained type for response rows that knows that the reference
// agent never returns custom scalars that are JSON objects
type ProjectedRow = {
[fieldName: string]: ScalarValue | QueryResponse
}
// We need a more constrained version of QueryResponse that uses ProjectedRow for rows
type QueryResponse = {
aggregates?: Record<string, ScalarValue> | null,
rows?: ProjectedRow[] | null
}
const prettyPrintBinaryComparisonOperator = (operator: BinaryComparisonOperator): string => {
switch (operator) {
case "greater_than": return ">";
@ -152,28 +160,141 @@ const makeFilterPredicate = (expression: Expression | null, getComparisonColumnV
return expression ? evaluate(expression) : true;
};
const sortRows = (rows: Record<string, ScalarValue>[], orderBy: OrderBy[]): Record<string, ScalarValue>[] =>
rows.sort((lhs, rhs) =>
orderBy.reduce((accum, { column, ordering }) => {
if (accum !== 0) {
return accum;
const buildQueryForPathedOrderByElement = (orderByElement: OrderByElement, orderByRelations: Record<RelationshipName, OrderByRelation>): Query => {
const [relationshipName, ...remainingPath] = orderByElement.target_path;
if (relationshipName === undefined) {
switch (orderByElement.target.type) {
case "column":
return {
fields: {
[orderByElement.target.column]: { type: "column", column: orderByElement.target.column }
}
};
case "single_column_aggregate":
return {
aggregates: {
[orderByElement.target.column]: { type: "single_column", column: orderByElement.target.column, function: orderByElement.target.function }
}
};
case "star_count_aggregate":
return {
aggregates: {
"count": { type: "star_count" }
}
};
default:
return unreachable(orderByElement.target["type"]);
}
} else {
const innerOrderByElement = { ...orderByElement, target_path: remainingPath };
const orderByRelation = orderByRelations[relationshipName];
const subquery = {
...buildQueryForPathedOrderByElement(innerOrderByElement, orderByRelation.subrelations),
where: orderByRelation.where
}
return {
fields: {
[relationshipName]: { type: "relationship", relationship: relationshipName, query: subquery }
}
const leftVal: ScalarValue = coerceUndefinedToNull(lhs[column]);
const rightVal: ScalarValue = coerceUndefinedToNull(rhs[column]);
const compared =
leftVal === null
? 1
: rightVal === null
? -1
: leftVal === rightVal
? 0
: leftVal < rightVal
? -1
: 1;
};
}
};
return ordering === "desc" ? -compared : compared;
}, 0)
);
const extractResultFromOrderByElementQueryResponse = (orderByElement: OrderByElement, response: QueryResponse): ScalarValue => {
const [relationshipName, ...remainingPath] = orderByElement.target_path;
const rows = response.rows ?? [];
const aggregates = response.aggregates ?? {};
if (relationshipName === undefined) {
switch (orderByElement.target.type) {
case "column":
if (rows.length > 1)
throw new Error(`Unexpected number of rows (${rows.length}) returned by order by element query`);
const fieldValue = rows.length === 1 ? rows[0][orderByElement.target.column] : null;
if (fieldValue !== null && typeof fieldValue === "object")
throw new Error("Column order by target path did not end in a column field value");
return coerceUndefinedToNull(fieldValue);
case "single_column_aggregate":
return aggregates[orderByElement.target.column];
case "star_count_aggregate":
return aggregates["count"];
default:
return unreachable(orderByElement.target["type"]);
}
} else {
if (rows.length > 1)
throw new Error(`Unexpected number of rows (${rows.length}) returned by order by element query`);
const fieldValue = rows.length === 1 ? rows[0][relationshipName] : null;
if (fieldValue === null || typeof fieldValue !== "object")
throw new Error(`Found a column field value in the middle of a order by target path: ${orderByElement.target_path}`);
const innerOrderByElement = { ...orderByElement, target_path: remainingPath };
return extractResultFromOrderByElementQueryResponse(innerOrderByElement, fieldValue);
}
};
const makeGetOrderByElementValue = (findRelationship: (relationshipName: RelationshipName) => Relationship, performQuery: (tableName: TableName, query: Query) => QueryResponse) => (orderByElement: OrderByElement, row: Record<string, ScalarValue>, orderByRelations: Record<RelationshipName, OrderByRelation>): ScalarValue => {
const [relationshipName, ...remainingPath] = orderByElement.target_path;
if (relationshipName === undefined) {
if (orderByElement.target.type !== "column")
throw new Error(`Cannot perform an order by target of type ${orderByElement.target.type} on the current table. Only column-typed targets are supported.`)
return coerceUndefinedToNull(row[orderByElement.target.column]);
} else {
const relationship = findRelationship(relationshipName);
const orderByRelation = orderByRelations[relationshipName];
const innerOrderByElement = { ...orderByElement, target_path: remainingPath };
const query = {
...buildQueryForPathedOrderByElement(innerOrderByElement, orderByRelation.subrelations),
where: orderByRelation.where
};
const subquery = addRelationshipFilterToQuery(row, relationship, query);
if (subquery === null) {
return null;
} else {
const queryResponse = performQuery(relationship.target_table, subquery);
return extractResultFromOrderByElementQueryResponse(innerOrderByElement, queryResponse);
}
}
};
const sortRows = (rows: Record<string, ScalarValue>[], orderBy: OrderBy, getOrderByElementValue: (orderByElement: OrderByElement, row: Record<string, ScalarValue>, orderByRelations: Record<RelationshipName, OrderByRelation>) => ScalarValue): Record<string, ScalarValue>[] =>
rows
.map<[Record<string, ScalarValue>, ScalarValue[]]>(row => [row, []])
.sort(([lhs, lhsValueCache], [rhs, rhsValueCache]) => {
return orderBy.elements.reduce((accum, orderByElement, orderByElementIndex) => {
if (accum !== 0) {
return accum;
}
const leftVal: ScalarValue =
lhsValueCache[orderByElementIndex] !== undefined
? lhsValueCache[orderByElementIndex]
: lhsValueCache[orderByElementIndex] = getOrderByElementValue(orderByElement, lhs, orderBy.relations);
const rightVal: ScalarValue =
rhsValueCache[orderByElementIndex] !== undefined
? rhsValueCache[orderByElementIndex]
: rhsValueCache[orderByElementIndex] = getOrderByElementValue(orderByElement, rhs, orderBy.relations);
const compared =
leftVal === null
? 1
: rightVal === null
? -1
: leftVal === rightVal
? 0
: leftVal < rightVal
? -1
: 1;
return orderByElement.order_direction === "desc" ? -compared : compared;
}, 0)
})
.map(([row, _valueCache]) => row);
const paginateRows = (rows: Record<string, ScalarValue>[], offset: number | null, limit: number | null): Record<string, ScalarValue>[] => {
const start = offset ?? 0;
@ -380,9 +501,10 @@ export const queryData = (getTable: (tableName: TableName) => Record<string, Sca
}
const findRelationship = makeFindRelationship(queryRequest.table_relationships, tableName);
const getComparisonColumnValues = makeGetComparisonColumnValues(findRelationship, performQuery);
const getOrderByElementValue = makeGetOrderByElementValue(findRelationship, performQuery);
const filteredRows = rows.filter(makeFilterPredicate(query.where ?? null, getComparisonColumnValues));
const sortedRows = sortRows(filteredRows, query.order_by ?? []);
const sortedRows = query.order_by ? sortRows(filteredRows, query.order_by, getOrderByElementValue) : filteredRows;
const paginatedRows = paginateRows(sortedRows, query.offset ?? null, query.limit ?? null);
const projectedRows = query.fields
? paginatedRows.map(projectRow(query.fields, findRelationship, performQuery))

View File

@ -779,12 +779,7 @@
"type": "number"
},
"order_by": {
"description": "Optionally order the results by the value of one or more fields",
"items": {
"$ref": "#/components/schemas/OrderBy"
},
"nullable": true,
"type": "array"
"$ref": "#/components/schemas/OrderBy"
},
"where": {
"$ref": "#/components/schemas/Expression"
@ -1234,26 +1229,151 @@
],
"type": "object"
},
"OrderType": {
"OrderByRelation": {
"properties": {
"subrelations": {
"additionalProperties": {
"$ref": "#/components/schemas/OrderByRelation"
},
"description": "Further relationships to follow from the relationship's target table. The key of the map is the relationship name.",
"type": "object"
},
"where": {
"$ref": "#/components/schemas/Expression"
}
},
"required": [
"subrelations"
],
"type": "object"
},
"OrderByStarCountAggregate": {
"properties": {
"type": {
"enum": [
"star_count_aggregate"
],
"type": "string"
}
},
"required": [
"type"
],
"type": "object"
},
"OrderBySingleColumnAggregate": {
"properties": {
"column": {
"description": "The column to apply the aggregation function to",
"type": "string"
},
"function": {
"$ref": "#/components/schemas/SingleColumnAggregateFunction"
},
"type": {
"enum": [
"single_column_aggregate"
],
"type": "string"
}
},
"required": [
"function",
"column",
"type"
],
"type": "object"
},
"OrderByColumn": {
"properties": {
"column": {
"type": "string"
},
"type": {
"enum": [
"column"
],
"type": "string"
}
},
"required": [
"column",
"type"
],
"type": "object"
},
"OrderByTarget": {
"discriminator": {
"mapping": {
"column": "OrderByColumn",
"single_column_aggregate": "OrderBySingleColumnAggregate",
"star_count_aggregate": "OrderByStarCountAggregate"
},
"propertyName": "type"
},
"oneOf": [
{
"$ref": "#/components/schemas/OrderByStarCountAggregate"
},
{
"$ref": "#/components/schemas/OrderBySingleColumnAggregate"
},
{
"$ref": "#/components/schemas/OrderByColumn"
}
]
},
"OrderDirection": {
"enum": [
"asc",
"desc"
],
"type": "string"
},
"OrderBy": {
"OrderByElement": {
"properties": {
"column": {
"description": "Column to order by",
"type": "string"
"order_direction": {
"$ref": "#/components/schemas/OrderDirection"
},
"ordering": {
"$ref": "#/components/schemas/OrderType"
"target": {
"$ref": "#/components/schemas/OrderByTarget"
},
"target_path": {
"description": "The relationship path from the current query table to the table that contains the target to order by. This is always non-empty for aggregate order by targets",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"column",
"ordering"
"target_path",
"target",
"order_direction"
],
"type": "object"
},
"OrderBy": {
"nullable": true,
"properties": {
"elements": {
"description": "The elements to order by, in priority order",
"items": {
"$ref": "#/components/schemas/OrderByElement"
},
"type": "array"
},
"relations": {
"additionalProperties": {
"$ref": "#/components/schemas/OrderByRelation"
},
"description": "A map of relationships from the current query table to target tables. The key of the map is the relationship name. The relationships are used within the order by elements.",
"type": "object"
}
},
"required": [
"relations",
"elements"
],
"type": "object"
}

View File

@ -34,7 +34,13 @@ export type { OpenApiReference } from './models/OpenApiReference';
export type { OpenApiSchema } from './models/OpenApiSchema';
export type { OpenApiXml } from './models/OpenApiXml';
export type { OrderBy } from './models/OrderBy';
export type { OrderType } from './models/OrderType';
export type { OrderByColumn } from './models/OrderByColumn';
export type { OrderByElement } from './models/OrderByElement';
export type { OrderByRelation } from './models/OrderByRelation';
export type { OrderBySingleColumnAggregate } from './models/OrderBySingleColumnAggregate';
export type { OrderByStarCountAggregate } from './models/OrderByStarCountAggregate';
export type { OrderByTarget } from './models/OrderByTarget';
export type { OrderDirection } from './models/OrderDirection';
export type { OrExpression } from './models/OrExpression';
export type { Query } from './models/Query';
export type { QueryCapabilities } from './models/QueryCapabilities';

View File

@ -2,13 +2,17 @@
/* tslint:disable */
/* eslint-disable */
import type { OrderType } from './OrderType';
import type { OrderByElement } from './OrderByElement';
import type { OrderByRelation } from './OrderByRelation';
export type OrderBy = {
/**
* Column to order by
* The elements to order by, in priority order
*/
column: string;
ordering: OrderType;
elements: Array<OrderByElement>;
/**
* A map of relationships from the current query table to target tables. The key of the map is the relationship name. The relationships are used within the order by elements.
*/
relations: Record<string, OrderByRelation>;
};

View File

@ -0,0 +1,9 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OrderByColumn = {
column: string;
type: 'column';
};

View File

@ -0,0 +1,16 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { OrderByTarget } from './OrderByTarget';
import type { OrderDirection } from './OrderDirection';
export type OrderByElement = {
order_direction: OrderDirection;
target: OrderByTarget;
/**
* The relationship path from the current query table to the table that contains the target to order by. This is always non-empty for aggregate order by targets
*/
target_path: Array<string>;
};

View File

@ -0,0 +1,14 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { Expression } from './Expression';
export type OrderByRelation = {
/**
* Further relationships to follow from the relationship's target table. The key of the map is the relationship name.
*/
subrelations: Record<string, OrderByRelation>;
where?: Expression;
};

View File

@ -0,0 +1,15 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { SingleColumnAggregateFunction } from './SingleColumnAggregateFunction';
export type OrderBySingleColumnAggregate = {
/**
* The column to apply the aggregation function to
*/
column: string;
function: SingleColumnAggregateFunction;
type: 'single_column_aggregate';
};

View File

@ -0,0 +1,8 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type OrderByStarCountAggregate = {
type: 'star_count_aggregate';
};

View File

@ -0,0 +1,10 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { OrderByColumn } from './OrderByColumn';
import type { OrderBySingleColumnAggregate } from './OrderBySingleColumnAggregate';
import type { OrderByStarCountAggregate } from './OrderByStarCountAggregate';
export type OrderByTarget = (OrderByStarCountAggregate | OrderBySingleColumnAggregate | OrderByColumn);

View File

@ -2,4 +2,4 @@
/* tslint:disable */
/* eslint-disable */
export type OrderType = 'asc' | 'desc';
export type OrderDirection = 'asc' | 'desc';

View File

@ -24,10 +24,7 @@ export type Query = {
* Optionally offset from the Nth result
*/
offset?: number | null;
/**
* Optionally order the results by the value of one or more fields
*/
order_by?: Array<OrderBy> | null;
order_by?: OrderBy;
where?: Expression;
};

View File

@ -1358,5 +1358,6 @@ test-suite tests-dc-api
, Test.QuerySpec
, Test.QuerySpec.AggregatesSpec
, Test.QuerySpec.BasicSpec
, Test.QuerySpec.OrderBySpec
, Test.QuerySpec.RelationshipsSpec
, Test.SchemaSpec

View File

@ -3,6 +3,7 @@
module Hasura.Backends.DataConnector.API.V0.Aggregate
( Aggregate (..),
SingleColumnAggregate (..),
singleColumnAggregateObjectCodec,
ColumnCountAggregate (..),
SingleColumnAggregateFunction (..),
)

View File

@ -1,53 +1,113 @@
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedLists #-}
--
module Hasura.Backends.DataConnector.API.V0.OrderBy
( OrderBy (..),
OrderType (..),
OrderByRelation (..),
OrderByElement (..),
OrderByTarget (..),
OrderDirection (..),
)
where
--------------------------------------------------------------------------------
import Autodocodec
import Autodocodec.OpenAPI ()
import Control.DeepSeq (NFData)
import Data.Aeson (FromJSON, ToJSON)
import Data.Data (Data)
import Data.HashMap.Strict (HashMap)
import Data.HashMap.Strict qualified as HashMap
import Data.Hashable (Hashable)
import Data.List.NonEmpty (NonEmpty)
import Data.OpenApi (ToSchema)
import GHC.Generics (Generic)
import Hasura.Backends.DataConnector.API.V0.Aggregate qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Expression qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Relationships qualified as API.V0
import Prelude
--------------------------------------------------------------------------------
data OrderBy = OrderBy
{ column :: API.V0.ColumnName,
ordering :: OrderType
{ _obRelations :: HashMap API.V0.RelationshipName OrderByRelation,
_obElements :: NonEmpty OrderByElement
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Hashable, NFData)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderBy
instance HasCodec OrderBy where
codec =
object "OrderBy" $
OrderBy
<$> requiredField "column" "Column to order by" .= column
<*> requiredField "ordering" "Ordering" .= ordering
<$> requiredField "relations" "A map of relationships from the current query table to target tables. The key of the map is the relationship name. The relationships are used within the order by elements." .= _obRelations
<*> requiredField "elements" "The elements to order by, in priority order" .= _obElements
--------------------------------------------------------------------------------
data OrderByRelation = OrderByRelation
{ _obrWhere :: Maybe API.V0.Expression,
_obrSubrelations :: HashMap API.V0.RelationshipName OrderByRelation
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Hashable, NFData)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderByRelation
data OrderType
instance HasCodec OrderByRelation where
codec =
named "OrderByRelation" $
object "OrderByRelation" $
OrderByRelation
<$> optionalFieldOrNull "where" "An expression to apply to the relationship's target table to filter it" .= _obrWhere
<*> requiredField "subrelations" "Further relationships to follow from the relationship's target table. The key of the map is the relationship name." .= _obrSubrelations
data OrderByElement = OrderByElement
{ _obeTargetPath :: [API.V0.RelationshipName],
_obeTarget :: OrderByTarget,
_obeOrderDirection :: OrderDirection
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderByElement
instance HasCodec OrderByElement where
codec =
object "OrderByElement" $
OrderByElement
<$> requiredField "target_path" "The relationship path from the current query table to the table that contains the target to order by. This is always non-empty for aggregate order by targets" .= _obeTargetPath
<*> requiredField "target" "The target column or aggregate to order by" .= _obeTarget
<*> requiredField "order_direction" "The direction of ordering to apply" .= _obeOrderDirection
data OrderByTarget
= OrderByColumn API.V0.ColumnName
| OrderByStarCountAggregate
| OrderBySingleColumnAggregate API.V0.SingleColumnAggregate
deriving stock (Data, Eq, Generic, Ord, Show)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderByTarget
instance HasCodec OrderByTarget where
codec =
object "OrderByTarget" $
discriminatedUnionCodec "type" enc dec
where
columnCodec = requiredField' "column"
starAggregateCodec = pureCodec ()
singleColumnAggregateCodec = API.V0.singleColumnAggregateObjectCodec
enc = \case
OrderByColumn c -> ("column", mapToEncoder c columnCodec)
OrderByStarCountAggregate -> ("star_count_aggregate", mapToEncoder () starAggregateCodec)
OrderBySingleColumnAggregate agg -> ("single_column_aggregate", mapToEncoder agg singleColumnAggregateCodec)
dec =
HashMap.fromList
[ ("column", ("OrderByColumn", mapToDecoder OrderByColumn columnCodec)),
("star_count_aggregate", ("OrderByStarCountAggregate", mapToDecoder (const OrderByStarCountAggregate) starAggregateCodec)),
("single_column_aggregate", ("OrderBySingleColumnAggregate", mapToDecoder OrderBySingleColumnAggregate singleColumnAggregateCodec))
]
data OrderDirection
= Ascending
| Descending
deriving stock (Data, Eq, Generic, Ord, Show, Enum, Bounded)
deriving anyclass (Hashable, NFData)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderType
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec OrderDirection
instance HasCodec OrderType where
instance HasCodec OrderDirection where
codec =
named "OrderType" $
named "OrderDirection" $
stringConstCodec [(Ascending, "asc"), (Descending, "desc")]

View File

@ -38,7 +38,6 @@ import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as KM
import Data.Data (Data)
import Data.HashMap.Strict qualified as HashMap
import Data.List.NonEmpty (NonEmpty)
import Data.OpenApi (ToSchema)
import Data.Text (Text)
import Data.Text qualified as T
@ -78,7 +77,7 @@ data Query = Query
_qLimit :: Maybe Int,
_qOffset :: Maybe Int,
_qWhere :: Maybe API.V0.Expression,
_qOrderBy :: Maybe (NonEmpty API.V0.OrderBy)
_qOrderBy :: Maybe API.V0.OrderBy
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Query

View File

@ -45,7 +45,7 @@ instance Backend 'DataConnector where
type RawFunctionInfo 'DataConnector = XDisable
type FunctionArgument 'DataConnector = XDisable
type ConstraintName 'DataConnector = Unimplemented
type BasicOrderType 'DataConnector = IR.O.OrderType
type BasicOrderType 'DataConnector = IR.O.OrderDirection
type NullsOrderType 'DataConnector = Unimplemented
type CountType 'DataConnector = IR.A.CountAggregate
type Column 'DataConnector = IR.C.Name

View File

@ -2,7 +2,10 @@
module Hasura.Backends.DataConnector.IR.OrderBy
( OrderBy (..),
OrderType (..),
OrderByRelation (..),
OrderByElement (..),
OrderByTarget (..),
OrderDirection (..),
)
where
@ -10,26 +13,22 @@ where
import Data.Aeson (ToJSON)
import Data.Aeson qualified as J
import Data.Bifunctor (bimap)
import Data.HashMap.Strict qualified as HashMap
import Hasura.Backends.DataConnector.API qualified as API
import Hasura.Backends.DataConnector.IR.Aggregate qualified as IR.A
import Hasura.Backends.DataConnector.IR.Column qualified as IR.C
import Hasura.Backends.DataConnector.IR.Expression qualified as IR.E
import Hasura.Backends.DataConnector.IR.Relationships qualified as IR.R
import Hasura.Incremental (Cacheable)
import Hasura.Prelude
import Witch qualified
--------------------------------------------------------------------------------
-- | Indicates a particular sort order that should be applied based on some
-- 'Column.Name' returned within a data source query.
--
-- TODO: We should use a sum type like @Query.Field@ here so that we can handle
-- @order by@ constraints on object/array relationships as well.
--
-- cf. https://www.postgresql.org/docs/13/queries-order.html
--
-- NOTE: The 'ToJSON' instance is only intended for logging purposes.
data OrderBy = OrderBy
{ column :: IR.C.Name,
ordering :: OrderType
{ _obRelations :: HashMap IR.R.RelationshipName OrderByRelation,
_obElements :: NonEmpty OrderByElement
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Cacheable, Hashable, NFData)
@ -37,34 +36,78 @@ data OrderBy = OrderBy
instance ToJSON OrderBy where
toJSON = J.genericToJSON J.defaultOptions
instance Witch.From API.OrderBy OrderBy where
from API.OrderBy {column, ordering} =
OrderBy (Witch.from column) (Witch.from ordering)
instance Witch.From OrderBy API.OrderBy where
from OrderBy {column, ordering} =
API.OrderBy (Witch.from column) (Witch.from ordering)
from OrderBy {..} =
API.OrderBy
{ _obRelations = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList _obRelations,
_obElements = Witch.from <$> _obElements
}
--------------------------------------------------------------------------------
data OrderByRelation = OrderByRelation
{ _obrWhere :: Maybe IR.E.Expression,
_obrSubrelations :: HashMap IR.R.RelationshipName OrderByRelation
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Cacheable, Hashable, NFData)
-- | 'Column.Name's may be sorted in either ascending or descending order.
--
-- cf. https://www.postgresql.org/docs/13/queries-order.html
--
-- NOTE: The 'ToJSON' instance is only intended for logging purposes.
data OrderType
instance ToJSON OrderByRelation where
toJSON = J.genericToJSON J.defaultOptions
instance Witch.From OrderByRelation API.OrderByRelation where
from OrderByRelation {..} =
API.OrderByRelation
{ _obrWhere = Witch.from <$> _obrWhere,
_obrSubrelations = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList _obrSubrelations
}
data OrderByElement = OrderByElement
{ _obeTargetPath :: [IR.R.RelationshipName],
_obeTarget :: OrderByTarget,
_obeOrderDirection :: OrderDirection
}
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Cacheable, Hashable, NFData)
instance ToJSON OrderByElement where
toJSON = J.genericToJSON J.defaultOptions
instance Witch.From OrderByElement API.OrderByElement where
from OrderByElement {..} =
API.OrderByElement
{ _obeTargetPath = Witch.from <$> _obeTargetPath,
_obeTarget = Witch.from _obeTarget,
_obeOrderDirection = Witch.from _obeOrderDirection
}
data OrderByTarget
= OrderByColumn IR.C.Name
| OrderByStarCountAggregate
| OrderBySingleColumnAggregate IR.A.SingleColumnAggregate
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Cacheable, Hashable, NFData)
instance ToJSON OrderByTarget where
toJSON = J.genericToJSON J.defaultOptions
instance Witch.From OrderByTarget API.OrderByTarget where
from = \case
OrderByColumn name -> API.OrderByColumn $ Witch.from name
OrderByStarCountAggregate -> API.OrderByStarCountAggregate
OrderBySingleColumnAggregate aggregate -> API.OrderBySingleColumnAggregate $ Witch.from aggregate
data OrderDirection
= Ascending
| Descending
deriving stock (Data, Eq, Generic, Ord, Show)
deriving anyclass (Cacheable, Hashable, NFData)
instance ToJSON OrderType where
instance ToJSON OrderDirection where
toJSON = J.genericToJSON J.defaultOptions
instance Witch.From API.OrderType OrderType where
instance Witch.From API.OrderDirection OrderDirection where
from API.Ascending = Ascending
from API.Descending = Descending
instance Witch.From OrderType API.OrderType where
instance Witch.From OrderDirection API.OrderDirection where
from Ascending = API.Ascending
from Descending = API.Descending

View File

@ -63,7 +63,7 @@ data Query = Query
-- | Optionally constrain the results to satisfy some predicate.
_qWhere :: Maybe IR.E.Expression,
-- | Optionally order the results by the value of one or more fields.
_qOrderBy :: [IR.O.OrderBy]
_qOrderBy :: Maybe IR.O.OrderBy
}
deriving stock (Data, Eq, Generic, Ord, Show)
@ -79,7 +79,7 @@ instance Witch.From Query API.Query where
_qLimit = _qLimit,
_qOffset = _qOffset,
_qWhere = fmap Witch.from _qWhere,
_qOrderBy = nonEmpty $ fmap Witch.from _qOrderBy
_qOrderBy = Witch.from <$> _qOrderBy
}
memptyToNothing :: (Monoid m, Eq m) => m -> Maybe m

View File

@ -152,7 +152,7 @@ mkPlan session (SourceConfig {}) ir = do
Just expr -> BoolAnd [expr, _tpFilter (_asnPerm selectG)]
Nothing -> _tpFilter (_asnPerm selectG)
whereClause <- translateBoolExp [] tableName whereClauseWithPermissions
orderBy <- lift $ translateOrderBy (_saOrderBy $ _asnArgs selectG)
orderBy <- traverse (translateOrderBy tableName) (_saOrderBy $ _asnArgs selectG)
pure
IR.Q.Query
{ _qFields = _faaFields,
@ -170,25 +170,102 @@ mkPlan session (SourceConfig {}) ir = do
}
translateOrderBy ::
Maybe (NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector))) ->
m [IR.O.OrderBy]
translateOrderBy = \case
Nothing -> pure []
Just orderBys ->
do
NE.toList
<$> for orderBys \OrderByItemG {..} -> case obiColumn of
AOCColumn (ColumnInfo {ciColumn = dynColumnName}) ->
pure
IR.O.OrderBy
{ column = dynColumnName,
-- NOTE: Picking a default ordering.
ordering = fromMaybe IR.O.Ascending obiType
}
AOCObjectRelation {} ->
throw400 NotSupported "translateOrderBy: AOCObjectRelation unsupported in the Data Connector backend"
AOCArrayAggregation {} ->
throw400 NotSupported "translateOrderBy: AOCArrayAggregation unsupported in the Data Connector backend"
IR.T.Name ->
NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector)) ->
CPS.WriterT IR.R.TableRelationships m IR.O.OrderBy
translateOrderBy sourceTableName orderByItems = do
orderByElementsAndRelations <- for orderByItems \OrderByItemG {..} -> do
let orderDirection = fromMaybe IR.O.Ascending obiType
translateOrderByElement sourceTableName orderDirection [] obiColumn
relations <- lift . mergeOrderByRelations $ snd <$> orderByElementsAndRelations
pure
IR.O.OrderBy
{ _obRelations = relations,
_obElements = fst <$> orderByElementsAndRelations
}
translateOrderByElement ::
IR.T.Name ->
IR.O.OrderDirection ->
[IR.R.RelationshipName] ->
AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) ->
CPS.WriterT IR.R.TableRelationships m (IR.O.OrderByElement, HashMap IR.R.RelationshipName IR.O.OrderByRelation)
translateOrderByElement sourceTableName orderDirection targetReversePath = \case
AOCColumn (ColumnInfo {..}) ->
pure
( IR.O.OrderByElement
{ _obeTargetPath = reverse targetReversePath,
_obeTarget = IR.O.OrderByColumn ciColumn,
_obeOrderDirection = orderDirection
},
mempty
)
AOCObjectRelation relationshipInfo filterExp orderByElement -> do
(relationshipName, IR.R.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
(translatedOrderByElement, subOrderByRelations) <- translateOrderByElement _rTargetTable orderDirection (relationshipName : targetReversePath) orderByElement
targetTableWhereExp <- translateBoolExp [] _rTargetTable filterExp
let orderByRelations = HashMap.fromList [(relationshipName, IR.O.OrderByRelation (Just targetTableWhereExp) subOrderByRelations)]
pure (translatedOrderByElement, orderByRelations)
AOCArrayAggregation relationshipInfo filterExp aggregateOrderByElement -> do
(relationshipName, IR.R.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
orderByTarget <- case aggregateOrderByElement of
AAOCount ->
pure IR.O.OrderByStarCountAggregate
AAOOp aggFunctionTxt ColumnInfo {..} -> do
aggFunction <- lift $ translateSingleColumnAggregateFunction aggFunctionTxt
pure . IR.O.OrderBySingleColumnAggregate $ IR.A.SingleColumnAggregate aggFunction ciColumn
let translatedOrderByElement =
IR.O.OrderByElement
{ _obeTargetPath = reverse (relationshipName : targetReversePath),
_obeTarget = orderByTarget,
_obeOrderDirection = orderDirection
}
targetTableWhereExp <- translateBoolExp [] _rTargetTable filterExp
let orderByRelations = HashMap.fromList [(relationshipName, IR.O.OrderByRelation (Just targetTableWhereExp) mempty)]
pure (translatedOrderByElement, orderByRelations)
mergeOrderByRelations ::
Foldable f =>
f (HashMap IR.R.RelationshipName IR.O.OrderByRelation) ->
m (HashMap IR.R.RelationshipName IR.O.OrderByRelation)
mergeOrderByRelations orderByRelationsList =
foldM mergeMap mempty orderByRelationsList
where
mergeMap :: HashMap IR.R.RelationshipName IR.O.OrderByRelation -> HashMap IR.R.RelationshipName IR.O.OrderByRelation -> m (HashMap IR.R.RelationshipName IR.O.OrderByRelation)
mergeMap left right = foldM (\targetMap (relName, orderByRel) -> HashMap.alterF (maybe (pure $ Just orderByRel) (fmap Just . mergeOrderByRelation orderByRel)) relName targetMap) left $ HashMap.toList right
mergeOrderByRelation :: IR.O.OrderByRelation -> IR.O.OrderByRelation -> m IR.O.OrderByRelation
mergeOrderByRelation right left =
if IR.O._obrWhere left == IR.O._obrWhere right
then do
mergedSubrelations <- mergeMap (IR.O._obrSubrelations left) (IR.O._obrSubrelations right)
pure $ IR.O.OrderByRelation (IR.O._obrWhere left) mergedSubrelations
else throw500 "mergeOrderByRelations: Differing filter expressions found for the same table"
recordTableRelationshipFromRelInfo ::
IR.T.Name ->
RelInfo 'DataConnector ->
CPS.WriterT IR.R.TableRelationships m (IR.R.RelationshipName, IR.R.Relationship)
recordTableRelationshipFromRelInfo sourceTableName RelInfo {..} = do
let relationshipName = IR.R.mkRelationshipName riName
let relationshipType = case riType of
ObjRel -> IR.R.ObjectRelationship
ArrRel -> IR.R.ArrayRelationship
let relationship =
IR.R.Relationship
{ _rTargetTable = riRTable,
_rRelationshipType = relationshipType,
_rColumnMapping = riMapping
}
recordTableRelationship
sourceTableName
relationshipName
relationship
pure (relationshipName, relationship)
translateAnnFields ::
FieldPrefix ->
@ -236,7 +313,7 @@ mkPlan session (SourceConfig {}) ir = do
_qWhere = Just whereClause,
_qLimit = Nothing,
_qOffset = Nothing,
_qOrderBy = []
_qOrderBy = Nothing
}
)
AFArrayRelation (ASSimple arrayRelationSelect) -> do
@ -310,18 +387,7 @@ mkPlan session (SourceConfig {}) ir = do
AFCount countAggregate -> pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) (IR.A.Count countAggregate)
AFOp AggregateOp {..} -> do
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
aggFunction <- case _aoOp of
"avg" -> pure IR.A.Average
"max" -> pure IR.A.Max
"min" -> pure IR.A.Min
"stddev_pop" -> pure IR.A.StandardDeviationPopulation
"stddev_samp" -> pure IR.A.StandardDeviationSample
"stddev" -> pure IR.A.StandardDeviationSample
"sum" -> pure IR.A.Sum
"var_pop" -> pure IR.A.VariancePopulation
"var_samp" -> pure IR.A.VarianceSample
"variance" -> pure IR.A.VarianceSample
unknownFunc -> throw500 $ "translateAggregateField: Unknown aggregate function encountered: " <> unknownFunc
aggFunction <- translateSingleColumnAggregateFunction _aoOp
fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) ->
case columnField of
@ -338,6 +404,20 @@ mkPlan session (SourceConfig {}) ir = do
-- to us
pure mempty
translateSingleColumnAggregateFunction :: Text -> m IR.A.SingleColumnAggregateFunction
translateSingleColumnAggregateFunction = \case
"avg" -> pure IR.A.Average
"max" -> pure IR.A.Max
"min" -> pure IR.A.Min
"stddev_pop" -> pure IR.A.StandardDeviationPopulation
"stddev_samp" -> pure IR.A.StandardDeviationSample
"stddev" -> pure IR.A.StandardDeviationSample
"sum" -> pure IR.A.Sum
"var_pop" -> pure IR.A.VariancePopulation
"var_samp" -> pure IR.A.VarianceSample
"variance" -> pure IR.A.VarianceSample
unknownFunc -> throw500 $ "translateSingleColumnAggregateFunction: Unknown aggregate function encountered: " <> unknownFunc
prepareLiterals ::
UnpreparedValue 'DataConnector ->
m IR.S.Literal
@ -364,20 +444,8 @@ mkPlan session (SourceConfig {}) ir = do
BoolField (AVColumn c xs) ->
lift $ mkIfZeroOrMany IR.E.And <$> traverse (translateOp columnRelationshipReversePath (ciColumn c)) xs
BoolField (AVRelationship relationshipInfo boolExp) -> do
let relationshipName = IR.R.mkRelationshipName $ riName relationshipInfo
let targetTable = riRTable relationshipInfo
let relationshipType = case riType relationshipInfo of
ObjRel -> IR.R.ObjectRelationship
ArrRel -> IR.R.ArrayRelationship
recordTableRelationship
sourceTableName
relationshipName
IR.R.Relationship
{ _rTargetTable = targetTable,
_rRelationshipType = relationshipType,
_rColumnMapping = riMapping relationshipInfo
}
translateBoolExp (relationshipName : columnRelationshipReversePath) targetTable boolExp
(relationshipName, IR.R.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
translateBoolExp (relationshipName : columnRelationshipReversePath) _rTargetTable boolExp
BoolExists _ ->
lift $ throw400 NotSupported "The BoolExists expression type is not supported by the Data Connector backend"
where

View File

@ -4,6 +4,7 @@
module Hasura.Backends.DataConnector.API.V0.AggregateSpec
( spec,
genAggregate,
genSingleColumnAggregate,
)
where

View File

@ -1,40 +1,152 @@
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE QuasiQuotes #-}
module Hasura.Backends.DataConnector.API.V0.OrderBySpec (spec, genOrderBy, genOrderType) where
module Hasura.Backends.DataConnector.API.V0.OrderBySpec
( spec,
genOrderBy,
genOrderDirection,
)
where
import Data.Aeson.QQ.Simple (aesonQQ)
import Data.HashMap.Strict qualified as HashMap
import Hasura.Backends.DataConnector.API.V0
import Hasura.Backends.DataConnector.API.V0.AggregateSpec (genSingleColumnAggregate)
import Hasura.Backends.DataConnector.API.V0.ColumnSpec (genColumnName)
import Hasura.Backends.DataConnector.API.V0.ExpressionSpec (genExpression)
import Hasura.Backends.DataConnector.API.V0.RelationshipsSpec (genRelationshipName)
import Hasura.Prelude
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range (linear)
import Test.Aeson.Utils (jsonOpenApiProperties, testToFromJSONToSchema)
import Test.Hspec
spec :: Spec
spec = do
describe "OrderByTarget" $ do
describe "OrderByColumn" $
testToFromJSONToSchema
(OrderByColumn (ColumnName "test_column"))
[aesonQQ|
{ "type": "column",
"column": "test_column"
}
|]
describe "OrderByStarCountAggregate" $
testToFromJSONToSchema
(OrderByStarCountAggregate)
[aesonQQ|
{ "type": "star_count_aggregate" }
|]
describe "OrderBySingleColumnAggregate" $
testToFromJSONToSchema
(OrderBySingleColumnAggregate (SingleColumnAggregate Sum (ColumnName "test_column")))
[aesonQQ|
{ "type": "single_column_aggregate",
"function": "sum",
"column": "test_column"
}
|]
jsonOpenApiProperties genOrderByTarget
describe "OrderByElement" $ do
testToFromJSONToSchema
( OrderByElement
[RelationshipName "relation1", RelationshipName "relation2"]
(OrderByColumn (ColumnName "my_column_name"))
Ascending
)
[aesonQQ|
{ "target_path": ["relation1", "relation2"],
"target": {
"type": "column",
"column": "my_column_name"
},
"order_direction": "asc"
}
|]
jsonOpenApiProperties genOrderByElement
describe "OrderByRelation" $ do
testToFromJSONToSchema
( OrderByRelation
(Just $ And [])
(HashMap.fromList [(RelationshipName "relationship_name", (OrderByRelation Nothing mempty))])
)
[aesonQQ|
{ "where": {
"type": "and",
"expressions": []
},
"subrelations": {
"relationship_name": {
"subrelations": {}
}
}
}
|]
jsonOpenApiProperties genOrderByRelation
describe "OrderBy" $ do
testToFromJSONToSchema
(OrderBy (ColumnName "my_column_name") Ascending)
( OrderBy
(HashMap.fromList [(RelationshipName "relationship_name", (OrderByRelation Nothing mempty))])
(OrderByElement [] OrderByStarCountAggregate Ascending :| [])
)
[aesonQQ|
{ "column": "my_column_name",
"ordering": "asc"
{ "relations": {
"relationship_name": {
"subrelations": {}
}
},
"elements": [
{
"target_path": [],
"target": {
"type": "star_count_aggregate"
},
"order_direction": "asc"
}
]
}
|]
jsonOpenApiProperties genOrderBy
describe "OrderType" $ do
describe "OrderDirection" $ do
describe "Ascending" $
testToFromJSONToSchema Ascending [aesonQQ|"asc"|]
describe "Descending" $
testToFromJSONToSchema Descending [aesonQQ|"desc"|]
jsonOpenApiProperties genOrderType
jsonOpenApiProperties genOrderDirection
genOrderBy :: MonadGen m => m OrderBy
genOrderBy =
OrderBy
<$> genColumnName
<*> genOrderType
<$> (HashMap.fromList <$> Gen.list (linear 0 5) ((,) <$> genRelationshipName <*> genOrderByRelation))
<*> Gen.nonEmpty (linear 1 5) genOrderByElement
genOrderType :: MonadGen m => m OrderType
genOrderType = Gen.enumBounded
genOrderByRelation :: MonadGen m => m OrderByRelation
genOrderByRelation =
OrderByRelation
<$> Gen.maybe genExpression
-- Gen.small ensures the recursion will terminate as the size will shrink with each recursion
<*> Gen.small (HashMap.fromList <$> Gen.list (linear 0 5) ((,) <$> genRelationshipName <*> genOrderByRelation))
genOrderByElement :: MonadGen m => m OrderByElement
genOrderByElement =
OrderByElement
<$> Gen.list (linear 0 5) genRelationshipName
<*> genOrderByTarget
<*> genOrderDirection
genOrderByTarget :: MonadGen m => m OrderByTarget
genOrderByTarget =
Gen.choice
[ OrderByColumn <$> genColumnName,
pure OrderByStarCountAggregate,
OrderBySingleColumnAggregate <$> genSingleColumnAggregate
]
genOrderDirection :: MonadGen m => m OrderDirection
genOrderDirection = Gen.enumBounded

View File

@ -52,7 +52,7 @@ spec = do
_qLimit = Just 10,
_qOffset = Just 20,
_qWhere = Just $ And [],
_qOrderBy = Just [OrderBy (ColumnName "my_column_name") Ascending]
_qOrderBy = Just $ OrderBy [] (OrderByElement [] (OrderByColumn (ColumnName "my_column_name")) Ascending :| [])
}
testToFromJSONToSchema
query
@ -62,7 +62,18 @@ spec = do
"limit": 10,
"offset": 20,
"where": {"type": "and", "expressions": []},
"order_by": [{"column": "my_column_name", "ordering": "asc"}]
"order_by": {
"relations": {},
"elements": [
{ "target_path": [],
"target": {
"type": "column",
"column": "my_column_name"
},
"order_direction": "asc"
}
]
}
}
|]
jsonOpenApiProperties genQuery
@ -152,7 +163,7 @@ genQuery =
<*> Gen.maybe (Gen.int (linear 0 5))
<*> Gen.maybe (Gen.int (linear 0 5))
<*> Gen.maybe genExpression
<*> Gen.maybe (Gen.nonEmpty (linear 0 5) genOrderBy)
<*> Gen.maybe genOrderBy
genQueryRequest :: MonadGen m => m QueryRequest
genQueryRequest =

View File

@ -13,6 +13,7 @@ module Test.Data
albumsTableName,
albumsRelationshipName,
albumsRows,
albumsRowsById,
albumsTableRelationships,
artistRelationshipName,
tracksRelationshipName,
@ -42,6 +43,7 @@ module Test.Data
tracksTableRelationships,
invoiceLinesRelationshipName,
mediaTypeRelationshipName,
albumRelationshipName,
-- = Utilities
emptyQuery,
sortBy,
@ -59,6 +61,7 @@ module Test.Data
columnField,
comparisonColumn,
localComparisonColumn,
orderByColumn,
)
where
@ -169,6 +172,10 @@ albumsTableName = mkTableName "Album"
albumsRows :: [KeyMap API.FieldValue]
albumsRows = sortBy "AlbumId" $ readTableFromXmlIntoRows albumsTableName
albumsRowsById :: HashMap Scientific (KeyMap API.FieldValue)
albumsRowsById =
HashMap.fromList $ mapMaybe (\album -> (,album) <$> album ^? ix "AlbumId" . _ColumnFieldNumber) albumsRows
albumsTableRelationships :: API.TableRelationships
albumsTableRelationships =
let artistsJoinFieldMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
@ -257,11 +264,13 @@ tracksTableRelationships :: API.TableRelationships
tracksTableRelationships =
let invoiceLinesJoinFieldMapping = HashMap.fromList [(API.ColumnName "TrackId", API.ColumnName "TrackId")]
mediaTypeJoinFieldMapping = HashMap.fromList [(API.ColumnName "MediaTypeId", API.ColumnName "MediaTypeId")]
albumJoinFieldMapping = HashMap.fromList [(API.ColumnName "AlbumId", API.ColumnName "AlbumId")]
in API.TableRelationships
tracksTableName
( HashMap.fromList
[ (invoiceLinesRelationshipName, API.Relationship invoiceLinesTableName API.ArrayRelationship invoiceLinesJoinFieldMapping),
(mediaTypeRelationshipName, API.Relationship mediaTypesTableName API.ObjectRelationship mediaTypeJoinFieldMapping)
(mediaTypeRelationshipName, API.Relationship mediaTypesTableName API.ObjectRelationship mediaTypeJoinFieldMapping),
(albumRelationshipName, API.Relationship albumsTableName API.ObjectRelationship albumJoinFieldMapping)
]
)
@ -271,6 +280,9 @@ invoiceLinesRelationshipName = API.RelationshipName "InvoiceLines"
mediaTypeRelationshipName :: API.RelationshipName
mediaTypeRelationshipName = API.RelationshipName "MediaType"
albumRelationshipName :: API.RelationshipName
albumRelationshipName = API.RelationshipName "Album"
emptyQuery :: API.Query
emptyQuery = API.Query Nothing Nothing Nothing Nothing Nothing Nothing
@ -332,3 +344,7 @@ comparisonColumn path columnName = API.ComparisonColumn path $ API.ColumnName co
localComparisonColumn :: Text -> API.ComparisonColumn
localComparisonColumn columnName = comparisonColumn [] columnName
orderByColumn :: [API.RelationshipName] -> Text -> API.OrderDirection -> API.OrderByElement
orderByColumn targetPath columnName orderDirection =
API.OrderByElement targetPath (API.OrderByColumn $ API.ColumnName columnName) orderDirection

View File

@ -8,13 +8,15 @@ import Servant.Client (Client)
import Test.Hspec
import Test.QuerySpec.AggregatesSpec qualified
import Test.QuerySpec.BasicSpec qualified
import Test.QuerySpec.OrderBySpec qualified
import Test.QuerySpec.RelationshipsSpec qualified
import Prelude
spec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Capabilities -> Spec
spec api sourceName config Capabilities {..} = do
spec api sourceName config capabilities@Capabilities {..} = do
describe "query API" do
Test.QuerySpec.BasicSpec.spec api sourceName config
Test.QuerySpec.OrderBySpec.spec api sourceName config capabilities
when (isJust cRelationships) $
Test.QuerySpec.RelationshipsSpec.spec api sourceName config
Test.QuerySpec.AggregatesSpec.spec api sourceName config cRelationships

View File

@ -130,7 +130,7 @@ spec api sourceName config relationshipCapabilities = describe "Aggregate Querie
it "can get the max total from all rows, after applying pagination, filtering and ordering" $ do
let where' = ApplyBinaryComparisonOperator Equal (Data.localComparisonColumn "BillingCountry") (ScalarValue (String "USA"))
let orderBy = OrderBy (ColumnName "BillingPostalCode") Descending :| [OrderBy (ColumnName "InvoiceId") Ascending]
let orderBy = OrderBy mempty $ Data.orderByColumn [] "BillingPostalCode" Descending :| [Data.orderByColumn [] "InvoiceId" Ascending]
let aggregates = KeyMap.fromList [("max", SingleColumn $ SingleColumnAggregate Max (ColumnName "Total"))]
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ 20 >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy)
response <- (api // _query) sourceName config queryRequest
@ -231,7 +231,7 @@ spec api sourceName config relationshipCapabilities = describe "Aggregate Querie
("BillingCountry", Data.columnField "BillingCountry")
]
let where' = ApplyBinaryComparisonOperator Equal (Data.localComparisonColumn "BillingCountry") (ScalarValue (String "Canada"))
let orderBy = OrderBy (ColumnName "BillingAddress") Ascending :| [OrderBy (ColumnName "InvoiceId") Ascending]
let orderBy = OrderBy mempty $ Data.orderByColumn [] "BillingAddress" Ascending :| [Data.orderByColumn [] "InvoiceId" Ascending]
let aggregates = KeyMap.fromList [("min", SingleColumn $ SingleColumnAggregate Min (ColumnName "Total"))]
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qLimit ?~ 30 >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy)
response <- (api // _query) sourceName config queryRequest
@ -369,7 +369,7 @@ artistsWithAlbumsQuery modifySubquery =
("Name", Data.columnField "Name"),
("Albums", RelField $ RelationshipField Data.albumsRelationshipName albumsSubquery)
]
artistOrderBy = OrderBy (ColumnName "ArtistId") Ascending :| []
artistOrderBy = OrderBy mempty $ Data.orderByColumn [] "ArtistId" Ascending :| []
artistQuery = Data.emptyQuery & qFields ?~ artistFields & qOrderBy ?~ artistOrderBy
artistsTableRelationships = Data.onlyKeepRelationships [Data.albumsRelationshipName] Data.artistsTableRelationships
in QueryRequest Data.artistsTableName [artistsTableRelationships] artistQuery
@ -419,7 +419,7 @@ deeplyNestedArtistsQuery =
]
tracksAggregates = KeyMap.fromList [("aggregate_count", StarCount)]
tracksWhere = ApplyBinaryComparisonOperator LessThan (Data.localComparisonColumn "Milliseconds") (ScalarValue $ Number 300000)
tracksOrderBy = OrderBy (ColumnName "Name") Descending :| []
tracksOrderBy = OrderBy mempty $ Data.orderByColumn [] "Name" Descending :| []
tracksSubquery = Query (Just tracksFields) (Just tracksAggregates) Nothing Nothing (Just tracksWhere) (Just tracksOrderBy)
albumsFields =
KeyMap.fromList
@ -437,7 +437,7 @@ deeplyNestedArtistsQuery =
[ ApplyBinaryComparisonOperator GreaterThan (Data.localComparisonColumn "Name") (ScalarValue $ String "A"),
ApplyBinaryComparisonOperator LessThan (Data.localComparisonColumn "Name") (ScalarValue $ String "B")
]
artistOrderBy = OrderBy (ColumnName "Name") Descending :| []
artistOrderBy = OrderBy mempty $ Data.orderByColumn [] "Name" Descending :| []
artistQuery = Query (Just artistFields) Nothing (Just 3) (Just 1) (Just artistWhere) (Just artistOrderBy)
in QueryRequest
Data.artistsTableName

View File

@ -3,9 +3,6 @@ module Test.QuerySpec.BasicSpec (spec) where
import Control.Arrow ((>>>))
import Control.Lens (ix, (%~), (&), (?~), (^?))
import Data.Aeson.KeyMap qualified as KeyMap
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.Ord (Down (..))
import Hasura.Backends.DataConnector.API
import Servant.API (NamedRoutes)
import Servant.Client (Client, (//))
@ -68,36 +65,6 @@ spec api sourceName config = describe "Basic Queries" $ do
page1Artists `rowsShouldBe` take 10 allArtists
page2Artists `rowsShouldBe` take 10 (drop 10 allArtists)
describe "Order By" $ do
it "can use order by to order results in ascending order" $ do
let orderBy = OrderBy (ColumnName "Title") Ascending :| []
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums = sortOn (^? ix "Title") Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can use order by to order results in descending order" $ do
let orderBy = OrderBy (ColumnName "Title") Descending :| []
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums = sortOn (Down . (^? ix "Title")) Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can use multiple order bys to order results" $ do
let orderBy = OrderBy (ColumnName "ArtistId") Ascending :| [OrderBy (ColumnName "Title") Descending]
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums =
sortOn (\album -> (album ^? ix "ArtistId", Down (album ^? ix "Title"))) Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
describe "Where" $ do
it "can filter using an equality expression" $ do
let where' = ApplyBinaryComparisonOperator Equal (Data.localComparisonColumn "AlbumId") (ScalarValue (Number 2))

View File

@ -0,0 +1,266 @@
module Test.QuerySpec.OrderBySpec (spec) where
import Control.Arrow ((>>>))
import Control.Lens (ix, (&), (.~), (?~), (^.), (^?), _1, _2, _3, _Just)
import Control.Monad (when)
import Data.Aeson.KeyMap (KeyMap)
import Data.Aeson.KeyMap qualified as KeyMap
import Data.HashMap.Strict qualified as HashMap
import Data.List (sortOn)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Data.Maybe (isJust)
import Data.Ord (Down (..))
import Hasura.Backends.DataConnector.API
import Servant.API (NamedRoutes)
import Servant.Client (Client, (//))
import Test.Data qualified as Data
import Test.Expectations (jsonShouldBe, rowsShouldBe)
import Test.Hspec (Spec, describe, it)
import Prelude
spec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Capabilities -> Spec
spec api sourceName config Capabilities {..} = describe "Order By in Queries" $ do
it "can order results in ascending order" $ do
let orderBy = OrderBy mempty $ Data.orderByColumn [] "Title" Ascending :| []
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums = sortOn (^? ix "Title") Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can order results in descending order" $ do
let orderBy = OrderBy mempty $ Data.orderByColumn [] "Title" Descending :| []
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums = sortOn (Down . (^? ix "Title")) Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can use multiple order by elements to order results" $ do
let orderBy = OrderBy mempty $ Data.orderByColumn [] "ArtistId" Ascending :| [Data.orderByColumn [] "Title" Descending]
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
receivedAlbums <- (api // _query) sourceName config query
let expectedAlbums =
sortOn (\album -> (album ^? ix "ArtistId", Down (album ^? ix "Title"))) Data.albumsRows
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
when (isJust cRelationships) $ orderByWithRelationshipsSpec api sourceName config
orderByWithRelationshipsSpec :: Client IO (NamedRoutes Routes) -> SourceName -> Config -> Spec
orderByWithRelationshipsSpec api sourceName config = describe "involving relationships" $ do
it "can order results by a column in a related table" $ do
let orderByRelations = HashMap.fromList [(Data.artistRelationshipName, OrderByRelation Nothing mempty)]
let orderBy = OrderBy orderByRelations $ Data.orderByColumn [Data.artistRelationshipName] "Name" Ascending :| []
let query =
albumsQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships .~ [Data.onlyKeepRelationships [Data.artistRelationshipName] Data.albumsTableRelationships]
receivedAlbums <- (api // _query) sourceName config query
let getRelatedArtist (album :: KeyMap FieldValue) =
(album ^? ix "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> Data.artistsRowsById ^? ix artistId
let expectedAlbums =
Data.albumsRows
& fmap (\album -> (album, getRelatedArtist album))
& sortOn ((^? _2 . _Just . ix "Name"))
& fmap fst
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can order results by a column in a related table where the related table is filtered" $ do
let artistTableFilter = ApplyBinaryComparisonOperator GreaterThan (Data.localComparisonColumn "Name") (ScalarValue $ String "N")
let orderByRelations = HashMap.fromList [(Data.artistRelationshipName, OrderByRelation (Just artistTableFilter) mempty)]
let orderBy = OrderBy orderByRelations $ Data.orderByColumn [Data.artistRelationshipName] "Name" Ascending :| []
let query =
albumsQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships .~ [Data.onlyKeepRelationships [Data.artistRelationshipName] Data.albumsTableRelationships]
receivedAlbums <- (api // _query) sourceName config query
let getRelatedArtist (album :: KeyMap FieldValue) = do
artist <- (album ^? ix "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> Data.artistsRowsById ^? ix artistId
if artist ^? ix "Name" . Data._ColumnFieldString > Just "N"
then pure artist
else Nothing
let expectedAlbums =
Data.albumsRows
& fmap (\album -> (album, getRelatedArtist album))
& sortOn ((^? _2 . _Just . ix "Name") >>> toNullsLastOrdering)
& fmap fst
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
it "can order results by a column in a related table of a related table" $ do
let orderByRelations =
HashMap.fromList
[ ( Data.albumRelationshipName,
OrderByRelation
Nothing
( HashMap.fromList
[ ( Data.artistRelationshipName,
OrderByRelation
Nothing
mempty
)
]
)
)
]
let orderBy =
OrderBy orderByRelations $
NonEmpty.fromList
[ Data.orderByColumn [Data.albumRelationshipName, Data.artistRelationshipName] "Name" Descending,
Data.orderByColumn [] "Name" Ascending
]
let query =
tracksQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships
.~ [ Data.onlyKeepRelationships [Data.albumRelationshipName] Data.tracksTableRelationships,
Data.onlyKeepRelationships [Data.artistRelationshipName] Data.albumsTableRelationships
]
receivedTracks <- (api // _query) sourceName config query
let getRelatedArtist (track :: KeyMap FieldValue) = do
albumId <- track ^? ix "AlbumId" . Data._ColumnFieldNumber
album <- Data.albumsRowsById ^? ix albumId
artistId <- album ^? ix "ArtistId" . Data._ColumnFieldNumber
Data.artistsRowsById ^? ix artistId
let expectedTracks =
Data.tracksRows
& fmap (\track -> (Data.filterColumnsByQueryFields (_qrQuery tracksQueryRequest) track, getRelatedArtist track, track ^? ix "Name"))
& sortOn (\row -> (Down (row ^? _2 . _Just . ix "Name"), row ^. _3))
& fmap (^. _1)
Data.responseRows receivedTracks `rowsShouldBe` expectedTracks
_qrAggregates receivedTracks `jsonShouldBe` Nothing
it "can order results by an aggregate of a related table" $ do
let orderByRelations = HashMap.fromList [(Data.albumsRelationshipName, OrderByRelation Nothing mempty)]
let orderBy = OrderBy orderByRelations $ OrderByElement [Data.albumsRelationshipName] OrderByStarCountAggregate Descending :| []
let query =
artistsQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships .~ [Data.onlyKeepRelationships [Data.albumsRelationshipName] Data.artistsTableRelationships]
receivedArtists <- (api // _query) sourceName config query
let getAlbumsCount (artist :: KeyMap FieldValue) = do
artistId <- artist ^? ix "ArtistId" . Data._ColumnFieldNumber
let albums = filter (\album -> album ^? ix "ArtistId" . Data._ColumnFieldNumber == Just artistId) Data.albumsRows
pure $ length albums
let expectedArtists =
Data.artistsRows
& fmap (\artist -> (artist, getAlbumsCount artist))
& sortOn (Down . (^. _2))
& fmap fst
Data.responseRows receivedArtists `rowsShouldBe` expectedArtists
_qrAggregates receivedArtists `jsonShouldBe` Nothing
it "can order results by an aggregate of a related table where the related table is filtered" $ do
let albumTableFilter = ApplyBinaryComparisonOperator GreaterThan (Data.localComparisonColumn "Title") (ScalarValue $ String "N")
let orderByRelations = HashMap.fromList [(Data.albumsRelationshipName, OrderByRelation (Just albumTableFilter) mempty)]
let orderBy = OrderBy orderByRelations $ OrderByElement [Data.albumsRelationshipName] OrderByStarCountAggregate Descending :| []
let query =
artistsQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships .~ [Data.onlyKeepRelationships [Data.albumsRelationshipName] Data.artistsTableRelationships]
receivedArtists <- (api // _query) sourceName config query
let getAlbumsCount (artist :: KeyMap FieldValue) = do
artistId <- artist ^? ix "ArtistId" . Data._ColumnFieldNumber
let albums = filter (\album -> album ^? ix "ArtistId" . Data._ColumnFieldNumber == Just artistId && album ^? ix "Title" . Data._ColumnFieldString > Just "N") Data.albumsRows
pure $ length albums
let expectedArtists =
Data.artistsRows
& fmap (\artist -> (artist, getAlbumsCount artist))
& sortOn (Down . (^. _2))
& fmap fst
Data.responseRows receivedArtists `rowsShouldBe` expectedArtists
_qrAggregates receivedArtists `jsonShouldBe` Nothing
it "can order results by an aggregate of a related table's related table" $ do
let orderByRelations =
HashMap.fromList
[ ( Data.artistRelationshipName,
OrderByRelation
Nothing
( HashMap.fromList
[ ( Data.albumsRelationshipName,
OrderByRelation
Nothing
mempty
)
]
)
)
]
let orderBy =
OrderBy orderByRelations $
NonEmpty.fromList
[ OrderByElement [Data.artistRelationshipName, Data.albumsRelationshipName] OrderByStarCountAggregate Descending,
OrderByElement [] (OrderByColumn $ ColumnName "Title") Ascending
]
let query =
albumsQueryRequest
& qrQuery . qOrderBy ?~ orderBy
& qrTableRelationships
.~ [ Data.onlyKeepRelationships [Data.artistRelationshipName] Data.albumsTableRelationships,
Data.onlyKeepRelationships [Data.albumsRelationshipName] Data.artistsTableRelationships
]
receivedAlbums <- (api // _query) sourceName config query
let getTotalArtistAlbumsCount (album :: KeyMap FieldValue) = do
artistId <- album ^? ix "ArtistId" . Data._ColumnFieldNumber
let albums = filter (\album' -> album' ^? ix "ArtistId" . Data._ColumnFieldNumber == Just artistId) Data.albumsRows
pure $ length albums
let expectedArtists =
Data.albumsRows
& fmap (\album -> (album, getTotalArtistAlbumsCount album, album ^? ix "Title"))
& sortOn (\row -> (Down (row ^. _2), (row ^. _3)))
& fmap (^. _1)
Data.responseRows receivedAlbums `rowsShouldBe` expectedArtists
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
albumsQueryRequest :: QueryRequest
albumsQueryRequest =
let fields = KeyMap.fromList [("AlbumId", Data.columnField "AlbumId"), ("ArtistId", Data.columnField "ArtistId"), ("Title", Data.columnField "Title")]
query = Data.emptyQuery & qFields ?~ fields
in QueryRequest Data.albumsTableName [] query
artistsQueryRequest :: QueryRequest
artistsQueryRequest =
let fields = KeyMap.fromList [("ArtistId", Data.columnField "ArtistId"), ("Name", Data.columnField "Name")]
query = Data.emptyQuery & qFields ?~ fields
in QueryRequest Data.artistsTableName [] query
tracksQueryRequest :: QueryRequest
tracksQueryRequest =
let fields = KeyMap.fromList [("TrackId", Data.columnField "TrackId"), ("Name", Data.columnField "Name")]
query = Data.emptyQuery & qFields ?~ fields
in QueryRequest Data.tracksTableName [] query
data NullableOrdered a
= NullFirst
| Some a
| NullLast
deriving stock (Eq, Ord, Show)
toNullsLastOrdering :: Maybe a -> NullableOrdered a
toNullsLastOrdering = maybe NullLast Some

View File

@ -157,7 +157,7 @@ albumsWithArtistQuery modifySubquery =
artistsWithAlbumsQuery :: (Query -> Query) -> QueryRequest
artistsWithAlbumsQuery modifySubquery =
let albumFields = KeyMap.fromList [("AlbumId", Data.columnField "AlbumId"), ("Title", Data.columnField "Title")]
albumsSort = OrderBy (ColumnName "AlbumId") Ascending :| []
albumsSort = OrderBy mempty $ Data.orderByColumn [] "AlbumId" Ascending :| []
albumsSubquery = albumsQuery & qFields ?~ albumFields & qOrderBy ?~ albumsSort & modifySubquery
fields =
KeyMap.fromList
@ -170,7 +170,7 @@ artistsWithAlbumsQuery modifySubquery =
employeesWithCustomersQuery :: (Query -> Query) -> QueryRequest
employeesWithCustomersQuery modifySubquery =
let customersSort = OrderBy (ColumnName "CustomerId") Ascending :| []
let customersSort = OrderBy mempty $ Data.orderByColumn [] "CustomerId" Ascending :| []
customersSubquery = customersQuery & qOrderBy ?~ customersSort & modifySubquery
fields =
Data.queryFields employeesQuery

View File

@ -32,7 +32,7 @@ import Data.Aeson qualified as Aeson
import Data.IORef qualified as I
import Harness.Backend.DataConnector.MockAgent
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.Http (healthCheck)
import Harness.Http (RequestHeaders, healthCheck)
import Harness.Quoter.Yaml (yaml)
import Harness.Test.Fixture (BackendType (DataConnector), Options, SetupAction (..), defaultBackendTypeString)
import Harness.TestEnvironment (TestEnvironment)
@ -157,6 +157,8 @@ data TestCase = TestCase
_given :: MockConfig,
-- | The Graphql Query to test
_whenRequest :: Aeson.Value,
-- | The headers to use on the Graphql Query request
_whenRequestHeaders :: RequestHeaders,
-- | The expected HGE 'API.Query' value to be provided to the
-- agent. A @Nothing@ value indicates that the 'API.Query'
-- assertion should be skipped.
@ -181,6 +183,7 @@ defaultTestCase TestCaseRequired {..} =
TestCase
{ _given = _givenRequired,
_whenRequest = _whenRequestRequired,
_whenRequestHeaders = [],
_whenQuery = Nothing,
_whenConfig = Nothing,
_then = _thenRequired
@ -197,8 +200,9 @@ runMockedTest opts TestCase {..} (testEnvironment, MockAgentEnvironment {..}) =
-- Execute the GQL Query and assert on the result
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
( GraphqlEngine.postGraphqlWithHeaders
testEnvironment
_whenRequestHeaders
_whenRequest
)
_then

View File

@ -109,6 +109,188 @@ schema =
API.dtiPrimaryKey = Just [API.ColumnName "AlbumId"],
API.dtiDescription = Just "Collection of music albums created by artists"
},
API.TableInfo
{ API.dtiName = mkTableName "Customer",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "CustomerId",
API.dciType = API.NumberTy,
API.dciNullable = False,
API.dciDescription = Just "Customer primary key identifier"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "FirstName",
API.dciType = API.StringTy,
API.dciNullable = False,
API.dciDescription = Just "The customer's first name"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "LastName",
API.dciType = API.StringTy,
API.dciNullable = False,
API.dciDescription = Just "The customer's last name"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Company",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's company name"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Address",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's address line (street number, street)"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "City",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's address city"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "State",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's address state"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Country",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's address country"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "PostalCode",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's address postal code"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Phone",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's phone number"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Fax",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The customer's fax number"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Email",
API.dciType = API.StringTy,
API.dciNullable = False,
API.dciDescription = Just "The customer's email address"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "SupportRepId",
API.dciType = API.NumberTy,
API.dciNullable = True,
API.dciDescription = Just "The ID of the Employee who is this customer's support representative"
}
],
API.dtiPrimaryKey = Just [API.ColumnName "CustomerId"],
API.dtiDescription = Just "Collection of customers who can buy tracks"
},
API.TableInfo
{ API.dtiName = mkTableName "Employee",
API.dtiColumns =
[ API.ColumnInfo
{ API.dciName = API.ColumnName "EmployeeId",
API.dciType = API.NumberTy,
API.dciNullable = False,
API.dciDescription = Just "Employee primary key identifier"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "LastName",
API.dciType = API.StringTy,
API.dciNullable = False,
API.dciDescription = Just "The employee's last name"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "FirstName",
API.dciType = API.StringTy,
API.dciNullable = False,
API.dciDescription = Just "The employee's first name"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Title",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's job title"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "ReportsTo",
API.dciType = API.NumberTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's report"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "BirthDate",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's birth date"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "HireDate",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's hire date"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Address",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's address line (street number, street)"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "City",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's address city"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "State",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's address state"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Country",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's address country"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "PostalCode",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's address postal code"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Phone",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's phone number"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Fax",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's fax number"
},
API.ColumnInfo
{ API.dciName = API.ColumnName "Email",
API.dciType = API.StringTy,
API.dciNullable = True,
API.dciDescription = Just "The employee's email address"
}
],
API.dtiPrimaryKey = Just [API.ColumnName "EmployeeId"],
API.dtiDescription = Just "Collection of employees who work for the business"
},
API.TableInfo
{ API.dtiName = mkTableName "Genre",
API.dtiColumns =

View File

@ -197,7 +197,7 @@ tests opts = do
_qLimit = Just 3,
_qOffset = Nothing,
_qWhere = Just (API.And []),
_qOrderBy = Just (API.OrderBy (API.ColumnName "AlbumId") API.Ascending NE.:| [])
_qOrderBy = Just (API.OrderBy mempty (API.OrderByElement [] (API.OrderByColumn (API.ColumnName "AlbumId")) API.Ascending :| []))
}
}
)

View File

@ -7,6 +7,7 @@ where
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KM
import Data.ByteString (ByteString)
import Data.HashMap.Strict qualified as HashMap
import Data.List.NonEmpty qualified as NE
import Harness.Backend.DataConnector (TestCase (..))
@ -33,6 +34,9 @@ spec =
)
tests
testRoleName :: ByteString
testRoleName = "test-role"
sourceMetadata :: Aeson.Value
sourceMetadata =
let source = defaultSource DataConnector
@ -41,10 +45,25 @@ sourceMetadata =
name : *source
kind: *backendType
tables:
- table: [Album]
object_relationships:
- name: Artist
using:
manual_configuration:
remote_table: [Artist]
column_mapping:
ArtistId: ArtistId
- table: [Artist]
- table: [Genre]
- table: [MediaType]
- table: [Track]
object_relationships:
- name: Album
using:
manual_configuration:
remote_table: [Album]
column_mapping:
AlbumId: AlbumId
- name: Genre
using:
manual_configuration:
@ -57,6 +76,47 @@ sourceMetadata =
remote_table: [MediaType]
column_mapping:
MediaTypeId: MediaTypeId
- table: [Employee]
array_relationships:
- name: SupportRepForCustomers
using:
manual_configuration:
remote_table: [Customer]
column_mapping:
EmployeeId: SupportRepId
select_permissions:
- role: *testRoleName
permission:
columns:
- EmployeeId
- FirstName
- LastName
- Country
filter:
SupportRepForCustomers:
Country:
_ceq: [ "$", "Country" ]
- table: [Customer]
object_relationships:
- name: SupportRep
using:
manual_configuration:
remote_table: [Employee]
column_mapping:
SupportRepId: EmployeeId
select_permissions:
- role: *testRoleName
permission:
columns:
- CustomerId
- FirstName
- LastName
- Country
- SupportRepId
filter:
SupportRep:
Country:
_ceq: [ "$", "Country" ]
configuration: {}
|]
@ -194,5 +254,253 @@ tests opts = do
)
}
it "works with an order by that navigates relationships" $
DataConnector.runMockedTest opts $
let required =
DataConnector.TestCaseRequired
{ _givenRequired =
let albums =
[ [ ( "Album",
API.mkRelationshipFieldValue $
rowsResponse
[ [ ( "Artist",
API.mkRelationshipFieldValue $
rowsResponse
[[("Name", API.mkColumnFieldValue $ Aeson.String "Zeca Pagodinho")]]
)
]
]
),
("Name", API.mkColumnFieldValue $ Aeson.String "Camarão que Dorme e Onda Leva")
]
]
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
_whenRequestRequired =
[graphql|
query getTrack {
Track(order_by: [{Album: {Artist: {Name: desc}}}, { Name: asc }], limit: 1) {
Album {
Artist {
Name
}
}
Name
}
}
|],
_thenRequired =
[yaml|
data:
Track:
- Album:
Artist:
Name: Zeca Pagodinho
Name: Camarão que Dorme e Onda Leva
|]
}
in (DataConnector.defaultTestCase required)
{ _whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName ("Track" :| []),
_qrTableRelationships =
[ API.TableRelationships
{ _trSourceTable = API.TableName ("Track" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Album",
API.Relationship
{ _rTargetTable = API.TableName ("Album" :| []),
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "AlbumId", API.ColumnName "AlbumId")]
}
)
]
},
API.TableRelationships
{ _trSourceTable = API.TableName ("Album" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "Artist",
API.Relationship
{ _rTargetTable = API.TableName ("Artist" :| []),
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
}
)
]
}
],
_qrQuery =
API.Query
{ _qFields =
Just $
KM.fromList
[ ("Name", API.ColumnField (API.ColumnName "Name")),
( "Album",
API.RelField
( API.RelationshipField
(API.RelationshipName "Album")
API.Query
{ _qFields =
Just $
KM.fromList
[ ( "Artist",
API.RelField
( API.RelationshipField
(API.RelationshipName "Artist")
API.Query
{ _qFields =
Just $
KM.fromList
[ ("Name", API.ColumnField (API.ColumnName "Name"))
],
_qAggregates = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Just (API.And []),
_qOrderBy = Nothing
}
)
)
],
_qAggregates = Nothing,
_qLimit = Nothing,
_qOffset = Nothing,
_qWhere = Just (API.And []),
_qOrderBy = Nothing
}
)
)
],
_qAggregates = Nothing,
_qLimit = Just 1,
_qOffset = Nothing,
_qWhere = Just (API.And []),
_qOrderBy =
Just $
API.OrderBy
( HashMap.fromList
[ ( API.RelationshipName "Album",
API.OrderByRelation
(Just $ API.And [])
( HashMap.fromList
[ ( API.RelationshipName "Artist",
API.OrderByRelation
(Just $ API.And [])
mempty
)
]
)
)
]
)
( NE.fromList
[ API.OrderByElement [API.RelationshipName "Album", API.RelationshipName "Artist"] (API.OrderByColumn (API.ColumnName "Name")) API.Descending,
API.OrderByElement [] (API.OrderByColumn (API.ColumnName "Name")) API.Ascending
]
)
}
}
)
}
it "works with an order by that navigates a relationship with table permissions" $
DataConnector.runMockedTest opts $
let required =
DataConnector.TestCaseRequired
{ _givenRequired =
let albums =
[ [ ("EmployeeId", API.mkColumnFieldValue $ Aeson.Number 3)
]
]
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
_whenRequestRequired =
[graphql|
query getEmployee {
Employee(limit: 1, order_by: {SupportRepForCustomers_aggregate: {count: desc}}) {
EmployeeId
}
}
|],
_thenRequired =
[yaml|
data:
Employee:
- EmployeeId: 3
|]
}
in (DataConnector.defaultTestCase required)
{ _whenRequestHeaders = [("X-Hasura-Role", testRoleName)],
_whenQuery =
Just
( API.QueryRequest
{ _qrTable = API.TableName ("Employee" :| []),
_qrTableRelationships =
[ API.TableRelationships
{ _trSourceTable = API.TableName ("Customer" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "SupportRep",
API.Relationship
{ _rTargetTable = API.TableName ("Employee" :| []),
_rRelationshipType = API.ObjectRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "SupportRepId", API.ColumnName "EmployeeId")]
}
)
]
},
API.TableRelationships
{ _trSourceTable = API.TableName ("Employee" :| []),
_trRelationships =
HashMap.fromList
[ ( API.RelationshipName "SupportRepForCustomers",
API.Relationship
{ _rTargetTable = API.TableName ("Customer" :| []),
_rRelationshipType = API.ArrayRelationship,
_rColumnMapping = HashMap.fromList [(API.ColumnName "EmployeeId", API.ColumnName "SupportRepId")]
}
)
]
}
],
_qrQuery =
API.Query
{ _qFields =
Just $
KM.fromList
[ ("EmployeeId", API.ColumnField (API.ColumnName "EmployeeId"))
],
_qAggregates = Nothing,
_qLimit = Just 1,
_qOffset = Nothing,
_qWhere =
Just $
API.ApplyBinaryComparisonOperator
API.Equal
(API.ComparisonColumn [API.RelationshipName "SupportRepForCustomers"] (API.ColumnName "Country"))
(API.AnotherColumn (API.ComparisonColumn [] (API.ColumnName "Country"))),
_qOrderBy =
Just $
API.OrderBy
( HashMap.fromList
[ ( API.RelationshipName "SupportRepForCustomers",
API.OrderByRelation
( Just $
API.ApplyBinaryComparisonOperator
API.Equal
(API.ComparisonColumn [API.RelationshipName "SupportRep"] (API.ColumnName "Country"))
(API.AnotherColumn (API.ComparisonColumn [] (API.ColumnName "Country")))
)
mempty
)
]
)
(API.OrderByElement [API.RelationshipName "SupportRepForCustomers"] API.OrderByStarCountAggregate API.Descending :| [])
}
}
)
}
rowsResponse :: [[(Aeson.Key, API.FieldValue)]] -> API.QueryResponse
rowsResponse rows = API.QueryResponse (Just $ KM.fromList <$> rows) Nothing

View File

@ -129,56 +129,6 @@ tests opts = describe "Queries" $ do
title: For Those About To Rock We Salute You
|]
it "works with order_by id asc" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(limit: 3, order_by: {id: asc}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 2
title: Balls to the Wall
- id: 3
title: Restless and Wild
|]
it "works with order_by id desc" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(limit: 3, order_by: {id: desc}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 347
title: Koyaanisqatsi (Soundtrack from the Motion Picture)
- id: 346
title: 'Mozart: Chamber Music'
- id: 345
title: 'Monteverdi: L''Orfeo'
|]
it "works with a primary key" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
@ -246,26 +196,26 @@ tests opts = describe "Queries" $ do
Name: "Balls to the Wall"
|]
it "works with pagination" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums (limit: 3, offset: 2) {
id
it "works with pagination" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums (limit: 3, offset: 2) {
id
}
}
}
|]
)
[yaml|
data:
albums:
- id: 3
- id: 4
- id: 5
|]
|]
)
[yaml|
data:
albums:
- id: 3
- id: 4
- id: 5
|]
describe "Array Relationships" $ do
it "joins on album id" $ \(testEnvironment, _) ->
@ -502,3 +452,122 @@ tests opts = describe "Queries" $ do
- id: 275
name: Philip Glass Ensemble
|]
describe "Order By Tests" $ do
it "works with order_by id asc" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(limit: 3, order_by: {id: asc}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 1
title: For Those About To Rock We Salute You
- id: 2
title: Balls to the Wall
- id: 3
title: Restless and Wild
|]
it "works with order_by id desc" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbum {
albums(limit: 3, order_by: {id: desc}) {
id
title
}
}
|]
)
[yaml|
data:
albums:
- id: 347
title: Koyaanisqatsi (Soundtrack from the Motion Picture)
- id: 346
title: 'Mozart: Chamber Music'
- id: 345
title: 'Monteverdi: L''Orfeo'
|]
it "can order by an aggregate" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getArtists {
artists(limit: 3, order_by: {albums_aggregate: {count: desc}}) {
name
albums_aggregate {
aggregate {
count
}
}
}
}
|]
)
[yaml|
data:
artists:
- name: Iron Maiden
albums_aggregate:
aggregate:
count: 21
- name: Led Zeppelin
albums_aggregate:
aggregate:
count: 14
- name: Deep Purple
albums_aggregate:
aggregate:
count: 11
|]
it "can order by a related field" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphql
testEnvironment
[graphql|
query getAlbums {
albums(limit: 4, order_by: [{artist: {name: asc}}, {title: desc}]) {
artist {
name
}
title
}
}
|]
)
[yaml|
data:
albums:
- artist:
name: AC/DC
title: Let There Be Rock
- artist:
name: AC/DC
title: For Those About To Rock We Salute You
- artist:
name: Aaron Copland & London Symphony Orchestra
title: A Copland Celebration, Vol. I
- artist:
name: Aaron Goldberg
title: Worlds
|]

View File

@ -264,3 +264,56 @@ tests opts = describe "SelectPermissionsSpec" $ do
FirstName: Martha
LastName: Silk
|]
it "Query that orders by a related table that has a permissions filter" $ \(testEnvironment, _) ->
shouldReturnYaml
opts
( GraphqlEngine.postGraphqlWithHeaders
testEnvironment
[("X-Hasura-Role", testRoleName)]
[graphql|
query getEmployee {
Employee(order_by: {SupportRepForCustomers_aggregate: {count: desc}}) {
FirstName
LastName
Country
SupportRepForCustomers {
Country
CustomerId
}
}
}
|]
)
[yaml|
data:
Employee:
- FirstName: Jane
LastName: Peacock
Country: Canada
SupportRepForCustomers:
- Country: Canada
CustomerId: 3
- Country: Canada
CustomerId: 15
- Country: Canada
CustomerId: 29
- Country: Canada
CustomerId: 30
- Country: Canada
CustomerId: 33
- FirstName: Steve
LastName: Johnson
Country: Canada
SupportRepForCustomers:
- Country: Canada
CustomerId: 14
- Country: Canada
CustomerId: 31
- FirstName: Margaret
LastName: Park
Country: Canada
SupportRepForCustomers:
- Country: Canada
CustomerId: 32
|]