mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
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:
parent
4be1ca758d
commit
ef0ca7dea2
@ -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.
|
||||
|
@ -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))
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
9
dc-agents/reference/src/types/models/OrderByColumn.ts
Normal file
9
dc-agents/reference/src/types/models/OrderByColumn.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type OrderByColumn = {
|
||||
column: string;
|
||||
type: 'column';
|
||||
};
|
||||
|
16
dc-agents/reference/src/types/models/OrderByElement.ts
Normal file
16
dc-agents/reference/src/types/models/OrderByElement.ts
Normal 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>;
|
||||
};
|
||||
|
14
dc-agents/reference/src/types/models/OrderByRelation.ts
Normal file
14
dc-agents/reference/src/types/models/OrderByRelation.ts
Normal 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;
|
||||
};
|
||||
|
@ -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';
|
||||
};
|
||||
|
@ -0,0 +1,8 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type OrderByStarCountAggregate = {
|
||||
type: 'star_count_aggregate';
|
||||
};
|
||||
|
10
dc-agents/reference/src/types/models/OrderByTarget.ts
Normal file
10
dc-agents/reference/src/types/models/OrderByTarget.ts
Normal 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);
|
||||
|
@ -2,4 +2,4 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type OrderType = 'asc' | 'desc';
|
||||
export type OrderDirection = 'asc' | 'desc';
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -3,6 +3,7 @@
|
||||
module Hasura.Backends.DataConnector.API.V0.Aggregate
|
||||
( Aggregate (..),
|
||||
SingleColumnAggregate (..),
|
||||
singleColumnAggregateObjectCodec,
|
||||
ColumnCountAggregate (..),
|
||||
SingleColumnAggregateFunction (..),
|
||||
)
|
||||
|
@ -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")]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -4,6 +4,7 @@
|
||||
module Hasura.Backends.DataConnector.API.V0.AggregateSpec
|
||||
( spec,
|
||||
genAggregate,
|
||||
genSingleColumnAggregate,
|
||||
)
|
||||
where
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
|
266
server/tests-dc-api/Test/QuerySpec/OrderBySpec.hs
Normal file
266
server/tests-dc-api/Test/QuerySpec/OrderBySpec.hs
Normal 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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 :| []))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|]
|
||||
|
@ -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
|
||||
|]
|
||||
|
Loading…
Reference in New Issue
Block a user