mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 10:46:25 +03:00
Structured Error Protocol for Data Connectors Agents - GDW-137
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/6061 Co-authored-by: Vishnu Bharathi <4211715+scriptnull@users.noreply.github.com> GitOrigin-RevId: 855d96378030f4e01b0c74b00e20e592e51e7a49
This commit is contained in:
parent
34d01caebb
commit
d54bb30d3b
@ -1457,6 +1457,19 @@ For example, here's a query that retrieves artists ordered descending by the cou
|
||||
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.
|
||||
|
||||
### Health endpoint
|
||||
|
||||
Agents must expose a `/health` endpoint which must return a 204 No Content HTTP response code if the agent is up and running. This does not mean that the agent is able to connect to any data source it performs queries against, only that the agent is running and can accept requests, even if some of those requests might fail because a dependant service is unavailable.
|
||||
|
||||
However, this endpoint can also be used to check whether the ability of the agent to talk to a particular data source is healthy. If the endpoint is sent the `X-Hasura-DataConnector-Config` and `X-Hasura-DataConnector-SourceName` headers, then the agent is expected to check that it can successfully talk to whatever data source is being specified by those headers. If it can do so, then it must return a 204 No Content response code.
|
||||
|
||||
### Reporting Errors
|
||||
|
||||
Any non-200 response code from an Agent (except for the `/health` endpoint) will be interpreted as an error. These should be handled gracefully by `graphql-engine` but provide limited details to users. If you wish to return structured error information to users you can return a status of `500` from the `/capabilities`, `/schema`, and `/query` endpoints with the following JSON format:
|
||||
|
||||
```
|
||||
{
|
||||
"type": "uncaught-error", // This may be extended to more types in future
|
||||
"message": String, // A plain-text message for display purposes
|
||||
"details": Value // An arbitrary JSON Value containing error details
|
||||
}
|
||||
```
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hasura/dc-api-types",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"description": "Hasura GraphQL Engine Data Connector Agent API types",
|
||||
"author": "Hasura (https://github.com/hasura/graphql-engine)",
|
||||
"license": "Apache-2.0",
|
||||
|
@ -17,6 +17,16 @@
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json;charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,6 +66,16 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid `X-Hasura-DataConnector-Config` or `X-Hasura-DataConnector-SourceName`"
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json;charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -104,6 +124,16 @@
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid `body` or `X-Hasura-DataConnector-Config` or `X-Hasura-DataConnector-SourceName`"
|
||||
},
|
||||
"500": {
|
||||
"content": {
|
||||
"application/json;charset=utf-8": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -682,6 +712,32 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorResponse": {
|
||||
"properties": {
|
||||
"details": {
|
||||
"additionalProperties": true,
|
||||
"default": null,
|
||||
"description": "Error details"
|
||||
},
|
||||
"message": {
|
||||
"description": "Error message",
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/ErrorResponseType"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorResponseType": {
|
||||
"enum": [
|
||||
"uncaught-error"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SchemaResponse": {
|
||||
"properties": {
|
||||
"tables": {
|
||||
|
@ -23,6 +23,8 @@ export type { ComparisonValue } from './models/ComparisonValue';
|
||||
export type { ConfigSchemaResponse } from './models/ConfigSchemaResponse';
|
||||
export type { Constraint } from './models/Constraint';
|
||||
export type { DataSchemaCapabilities } from './models/DataSchemaCapabilities';
|
||||
export type { ErrorResponse } from './models/ErrorResponse';
|
||||
export type { ErrorResponseType } from './models/ErrorResponseType';
|
||||
export type { ExistsExpression } from './models/ExistsExpression';
|
||||
export type { ExistsInTable } from './models/ExistsInTable';
|
||||
export type { ExplainCapabilities } from './models/ExplainCapabilities';
|
||||
|
18
dc-agents/dc-api-types/src/models/ErrorResponse.ts
Normal file
18
dc-agents/dc-api-types/src/models/ErrorResponse.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { ErrorResponseType } from './ErrorResponseType';
|
||||
|
||||
export type ErrorResponse = {
|
||||
/**
|
||||
* Error details
|
||||
*/
|
||||
details?: any;
|
||||
/**
|
||||
* Error message
|
||||
*/
|
||||
message: string;
|
||||
type?: ErrorResponseType;
|
||||
};
|
||||
|
5
dc-agents/dc-api-types/src/models/ErrorResponseType.ts
Normal file
5
dc-agents/dc-api-types/src/models/ErrorResponseType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type ErrorResponseType = 'uncaught-error';
|
10
dc-agents/package-lock.json
generated
10
dc-agents/package-lock.json
generated
@ -24,7 +24,7 @@
|
||||
},
|
||||
"dc-api-types": {
|
||||
"name": "@hasura/dc-api-types",
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
@ -631,7 +631,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^7.0.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify": "^3.29.0",
|
||||
"mathjs": "^11.0.0",
|
||||
"pino-pretty": "^8.0.0",
|
||||
@ -1389,7 +1389,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.1.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify": "^4.4.0",
|
||||
"fastify-metrics": "^9.2.1",
|
||||
"nanoid": "^3.3.4",
|
||||
@ -3122,7 +3122,7 @@
|
||||
"version": "file:reference",
|
||||
"requires": {
|
||||
"@fastify/cors": "^7.0.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
"@types/node": "^16.11.49",
|
||||
"@types/xml2js": "^0.4.11",
|
||||
@ -3613,7 +3613,7 @@
|
||||
"version": "file:sqlite",
|
||||
"requires": {
|
||||
"@fastify/cors": "^8.1.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
"@types/node": "^16.11.49",
|
||||
"@types/sqlite3": "^3.1.8",
|
||||
|
4
dc-agents/reference/package-lock.json
generated
4
dc-agents/reference/package-lock.json
generated
@ -10,7 +10,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^7.0.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify": "^3.29.0",
|
||||
"mathjs": "^11.0.0",
|
||||
"pino-pretty": "^8.0.0",
|
||||
@ -44,7 +44,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@hasura/dc-api-types": {
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
|
@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^7.0.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify": "^3.29.0",
|
||||
"mathjs": "^11.0.0",
|
||||
"pino-pretty": "^8.0.0",
|
||||
|
@ -9,11 +9,11 @@ The SQLite agent currently supports the following capabilities:
|
||||
|
||||
* [x] GraphQL Schema
|
||||
* [x] GraphQL Queries
|
||||
* [ ] GraphQL Mutations
|
||||
* [x] Relationships
|
||||
* [x] Aggregations
|
||||
* [x] Prometheus Metrics
|
||||
* [ ] Exposing Foreign-Key Information
|
||||
* [x] Exposing Foreign-Key Information
|
||||
* [ ] Mutations
|
||||
* [ ] Subscriptions
|
||||
* [ ] Streaming Subscriptions
|
||||
|
||||
@ -26,6 +26,7 @@ Note: You are able to get detailed metadata about the agent's capabilities by
|
||||
* SQLite `>= 3.38.0` or compiled in JSON support
|
||||
* Required for the json_group_array() and json_group_object() aggregate SQL functions
|
||||
* https://www.sqlite.org/json1.html#jgrouparray
|
||||
* Note: NPM is used for the [TS Types for the DC-API protocol](https://www.npmjs.com/package/@hasura/dc-api-types)
|
||||
|
||||
## Build & Run
|
||||
|
||||
@ -41,6 +42,15 @@ Or a simple dev-loop via `entr`:
|
||||
echo src/**/*.ts | xargs -n1 echo | DB_READONLY=y entr -r npm run start
|
||||
```
|
||||
|
||||
## Docker Build & Run
|
||||
|
||||
```
|
||||
> docker build . -t dc-sqlite-agent:latest
|
||||
> docker run -it --rm -p 8100:8100 dc-sqlite-agent:latest
|
||||
```
|
||||
|
||||
You will want to mount a volume with your database(s) so that they can be referenced in configuration.
|
||||
|
||||
## Options / Environment Variables
|
||||
|
||||
Note: Boolean flags `{FLAG}` can be provided as `1`, `true`, `yes`, or omitted and default to `false`.
|
||||
@ -57,6 +67,7 @@ Note: Boolean flags `{FLAG}` can be provided as `1`, `true`, `yes`, or omitted a
|
||||
| `PRETTY_PRINT_LOGS` | `{FLAG}` | `false` | Uses `pino-pretty` to pretty print request logs |
|
||||
| `LOG_LEVEL` | `fatal` \| `error` \| `info` \| `debug` \| `trace` \| `silent` | `info` | The minimum log level to output |
|
||||
| `METRICS` | `{FLAG}` | `false` | Enables a `/metrics` prometheus metrics endpoint.
|
||||
| `QUERY_LENGTH_LIMIT` | `INT` | `Infinity` | Puts a limit on the length of generated SQL before execution. |
|
||||
|
||||
## Agent usage
|
||||
|
||||
@ -69,16 +80,6 @@ The schema is exposed via introspection, but you can limit which tables are refe
|
||||
* Explicitly enumerating them via the `tables` field, or
|
||||
* Toggling the `include_sqlite_meta_tables` to include or exclude sqlite meta tables.
|
||||
|
||||
|
||||
## Docker Build & Run
|
||||
|
||||
```
|
||||
> docker build . -t dc-sqlite-agent:latest
|
||||
> docker run -it --rm -p 8100:8100 dc-sqlite-agent:latest
|
||||
```
|
||||
|
||||
You will want to mount a volume with your database(s) so that they can be referenced in configuration.
|
||||
|
||||
## Dataset
|
||||
|
||||
The dataset used for testing the reference agent is sourced from:
|
||||
@ -130,27 +131,6 @@ From the HGE repo.
|
||||
* [x] ORDER clause in aggregates breaks SQLite parser for some reason
|
||||
* [x] Check that looped exist check doesn't cause name conflicts
|
||||
* [ ] `NOT EXISTS IS NULL` != `EXISTS IS NOT NULL`, Example:
|
||||
sqlite> create table test(testid string);
|
||||
sqlite> .schema
|
||||
CREATE TABLE test(testid string);
|
||||
sqlite> select 1 where exists(select * from test where testid is null);
|
||||
sqlite> select 1 where exists(select * from test where testid is not null);
|
||||
sqlite> select 1 where not exists(select * from test where testid is null);
|
||||
1
|
||||
sqlite> select 1 where not exists(select * from test where testid is not null);
|
||||
1
|
||||
sqlite> insert into test(testid) values('foo');
|
||||
sqlite> insert into test(testid) values(NULL);
|
||||
sqlite> select * from test;
|
||||
foo
|
||||
|
||||
sqlite> select 1 where exists(select * from test where testid is null);
|
||||
1
|
||||
sqlite> select 1 where exists(select * from test where testid is not null);
|
||||
1
|
||||
sqlite> select 1 where not exists(select * from test where testid is null);
|
||||
sqlite> select 1 where exists(select * from test where testid is not null);
|
||||
1
|
||||
|
||||
# Known Bugs
|
||||
|
||||
|
4
dc-agents/sqlite/package-lock.json
generated
4
dc-agents/sqlite/package-lock.json
generated
@ -10,7 +10,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.1.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify": "^4.4.0",
|
||||
"fastify-metrics": "^9.2.1",
|
||||
"nanoid": "^3.3.4",
|
||||
@ -54,7 +54,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hasura/dc-api-types": {
|
||||
"version": "0.10.0",
|
||||
"version": "0.11.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.3",
|
||||
|
@ -22,7 +22,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.1.0",
|
||||
"@hasura/dc-api-types": "0.10.0",
|
||||
"@hasura/dc-api-types": "0.11.0",
|
||||
"fastify-metrics": "^9.2.1",
|
||||
"fastify": "^4.4.0",
|
||||
"nanoid": "^3.3.4",
|
||||
|
@ -35,6 +35,7 @@ export const configSchema: ConfigSchemaResponse = {
|
||||
config_schema: {
|
||||
type: "object",
|
||||
nullable: false,
|
||||
required: ["db"],
|
||||
properties: {
|
||||
db: {
|
||||
description: "The SQLite database file to use.",
|
||||
|
@ -4,7 +4,7 @@ import { getSchema } from './schema';
|
||||
import { explain, queryData } from './query';
|
||||
import { getConfig, tryGetConfig } from './config';
|
||||
import { capabilitiesResponse } from './capabilities';
|
||||
import { QueryResponse, SchemaResponse, QueryRequest, CapabilitiesResponse, ExplainResponse, RawRequest, RawResponse } from '@hasura/dc-api-types';
|
||||
import { QueryResponse, SchemaResponse, QueryRequest, CapabilitiesResponse, ExplainResponse, RawRequest, RawResponse, ErrorResponse } from '@hasura/dc-api-types';
|
||||
import { connect } from './db';
|
||||
import { envToBool, envToString } from './util';
|
||||
import metrics from 'fastify-metrics';
|
||||
@ -30,6 +30,20 @@ const server = Fastify({
|
||||
}
|
||||
})
|
||||
|
||||
server.setErrorHandler(function (error, _request, reply) {
|
||||
// Log error
|
||||
this.log.error(error)
|
||||
|
||||
const errorResponse: ErrorResponse = {
|
||||
type: "uncaught-error",
|
||||
message: "SQLite Agent: Uncaught Exception",
|
||||
details: error
|
||||
};
|
||||
|
||||
// Send error response
|
||||
reply.status(500).send(errorResponse);
|
||||
})
|
||||
|
||||
const METRICS_ENABLED = envToBool('METRICS');
|
||||
|
||||
if(METRICS_ENABLED) {
|
||||
@ -88,7 +102,12 @@ const sqlLogger = (sql: string): void => {
|
||||
server.log.debug({sql}, "Executed SQL");
|
||||
};
|
||||
|
||||
server.get<{ Reply: CapabilitiesResponse }>("/capabilities", async (request, _response) => {
|
||||
// NOTE:
|
||||
//
|
||||
// While an ErrorResponse is available it is not currently used as there are no errors anticipated.
|
||||
// It is included here for illustrative purposes.
|
||||
//
|
||||
server.get<{ Reply: CapabilitiesResponse | ErrorResponse }>("/capabilities", async (request, _response) => {
|
||||
server.log.info({ headers: request.headers, query: request.body, }, "capabilities.request");
|
||||
return capabilitiesResponse;
|
||||
});
|
||||
@ -99,12 +118,15 @@ server.get<{ Reply: SchemaResponse }>("/schema", async (request, _response) => {
|
||||
return getSchema(config, sqlLogger);
|
||||
});
|
||||
|
||||
server.post<{ Body: QueryRequest, Reply: QueryResponse }>("/query", async (request, _response) => {
|
||||
server.post<{ Body: QueryRequest, Reply: QueryResponse | ErrorResponse }>("/query", async (request, response) => {
|
||||
server.log.info({ headers: request.headers, query: request.body, }, "query.request");
|
||||
const end = queryHistogram.startTimer()
|
||||
const config = getConfig(request);
|
||||
const result = queryData(config, sqlLogger, request.body);
|
||||
const result : QueryResponse | ErrorResponse = await queryData(config, sqlLogger, request.body);
|
||||
end();
|
||||
if("message" in result) {
|
||||
response.statusCode = 500;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Config } from "./config";
|
||||
import { connect, SqlLogger } from "./db";
|
||||
import { coerceUndefinedToNull, omap, last, coerceUndefinedOrNullToEmptyRecord, envToBool, isEmptyObject, tableNameEquals, unreachable, logDeep } from "./util";
|
||||
import { coerceUndefinedToNull, omap, last, coerceUndefinedOrNullToEmptyRecord, envToBool, isEmptyObject, tableNameEquals, unreachable, logDeep, envToString, envToNum } from "./util";
|
||||
import {
|
||||
Expression,
|
||||
BinaryComparisonOperator,
|
||||
@ -20,6 +20,7 @@ import {
|
||||
UnaryComparisonOperator,
|
||||
ExplainResponse,
|
||||
ExistsExpression,
|
||||
ErrorResponse,
|
||||
} from "@hasura/dc-api-types";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
@ -507,12 +508,25 @@ function tag(t: string, s: string): string {
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
export async function queryData(config: Config, sqlLogger: SqlLogger, queryRequest: QueryRequest): Promise<QueryResponse> {
|
||||
export async function queryData(config: Config, sqlLogger: SqlLogger, queryRequest: QueryRequest): Promise<QueryResponse | ErrorResponse> {
|
||||
const db = connect(config, sqlLogger); // TODO: Should this be cached?
|
||||
const q = query(queryRequest);
|
||||
const [result, metadata] = await db.query(q);
|
||||
|
||||
return output(result);
|
||||
const query_length_limit = envToNum('QUERY_LENGTH_LIMIT', Infinity);
|
||||
if(q.length > query_length_limit) {
|
||||
const result: ErrorResponse =
|
||||
{
|
||||
message: `Generated SQL Query was too long (${q.length} > ${query_length_limit})`,
|
||||
details: {
|
||||
"query.length": q.length,
|
||||
"limit": query_length_limit
|
||||
}
|
||||
};
|
||||
return result;
|
||||
} else {
|
||||
const [result, metadata] = await db.query(q);
|
||||
return output(result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -7,6 +7,6 @@ export async function runRawOperation(config: Config, sqlLogger: SqlLogger, quer
|
||||
const [results, metadata] = await db.query(query.query);
|
||||
|
||||
return {
|
||||
rows: results as Array<Record<string, any>>
|
||||
rows: (results || []) as Array<Record<string, any>>
|
||||
};
|
||||
};
|
||||
|
@ -38,6 +38,11 @@ export function envToString(envVarName: string, defaultValue: string): string {
|
||||
return val === undefined ? defaultValue : val;
|
||||
}
|
||||
|
||||
export function envToNum(envVarName: string, defaultValue: number): number {
|
||||
const val = process.env[envVarName];
|
||||
return val === undefined ? defaultValue : Number(val);
|
||||
}
|
||||
|
||||
export function last<T>(x: Array<T>): T {
|
||||
return x[x.length - 1];
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ GENERATED_CABAL_FILES = $(foreach package_file,$(PACKAGE_YAML_FILES),$(wildcard
|
||||
|
||||
.PHONY: build-all
|
||||
## build-all: build all haskell packages, or "have i broken anything?"
|
||||
build-all: build build-tests build-integration-tests build-pro build-pro-tests build-multitenant build-multitenant-integration-tests
|
||||
build-all: build build-tests build-integration-tests build-pro build-pro-tests build-multitenant build-multitenant-integration-tests build-tests-dc-api
|
||||
|
||||
.PHONY: build
|
||||
## build: build non-pro graphql executable
|
||||
@ -23,6 +23,11 @@ build-tests: $(GENERATED_CABAL_FILES)
|
||||
build-integration-tests: $(GENERATED_CABAL_FILES)
|
||||
cabal build api-tests
|
||||
|
||||
.PHONY: build-tests-dc-api
|
||||
## build-dc-api-tests: build dc-api agent tests
|
||||
build-tests-dc-api: $(GENERATED_CABAL_FILES)
|
||||
cabal build tests-dc-api
|
||||
|
||||
.PHONY: build-pro
|
||||
## build-pro: build pro graphql executable
|
||||
build-pro: $(GENERATED_CABAL_FILES)
|
||||
|
@ -23,6 +23,7 @@ executable api-tests
|
||||
, postgresql-simple
|
||||
, safe-exceptions
|
||||
, split
|
||||
, sop-core
|
||||
, test-harness
|
||||
, text
|
||||
, unordered-containers
|
||||
@ -66,6 +67,7 @@ executable api-tests
|
||||
Test.DataConnector.MetadataApiSpec
|
||||
Test.DataConnector.MockAgent.AggregateQuerySpec
|
||||
Test.DataConnector.MockAgent.BasicQuerySpec
|
||||
Test.DataConnector.MockAgent.ErrorSpec
|
||||
Test.DataConnector.MockAgent.QueryRelationshipsSpec
|
||||
Test.DataConnector.MockAgent.TransformedConfigurationSpec
|
||||
Test.DataConnector.QuerySpec
|
||||
|
@ -97,7 +97,7 @@ tests opts = describe "Aggregate Query Tests" $ do
|
||||
)
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse response},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse response)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getArtist {
|
||||
@ -216,7 +216,7 @@ tests opts = describe "Aggregate Query Tests" $ do
|
||||
)
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> aggregatesAndRowsResponse aggregates rows},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (aggregatesAndRowsResponse aggregates rows)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getInvoices {
|
||||
|
@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
-- | Query Tests for Data Connector Backend using a Mock Agent
|
||||
@ -116,7 +117,7 @@ tests opts = do
|
||||
(API.FieldName "title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You")
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getAlbum {
|
||||
@ -174,7 +175,7 @@ tests opts = do
|
||||
(API.FieldName "title", API.mkColumnFieldValue $ Aeson.String "Restless and Wild")
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getAlbum {
|
||||
@ -233,7 +234,7 @@ tests opts = do
|
||||
[ (API.FieldName "CustomerId", API.mkColumnFieldValue $ Aeson.Number 3)
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getCustomers {
|
||||
|
@ -0,0 +1,114 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
-- | Tests for Error Conditions in Data Connector Backends
|
||||
module Test.DataConnector.MockAgent.ErrorSpec
|
||||
( spec,
|
||||
)
|
||||
where
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
import Data.Aeson qualified as Aeson
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.List.NonEmpty qualified as NE
|
||||
import Harness.Backend.DataConnector (TestCase (..))
|
||||
import Harness.Backend.DataConnector qualified as DataConnector
|
||||
import Harness.Quoter.Graphql (graphql)
|
||||
import Harness.Quoter.Yaml (yaml)
|
||||
import Harness.Test.BackendType (BackendType (..), defaultBackendTypeString, defaultSource)
|
||||
import Harness.Test.Fixture qualified as Fixture
|
||||
import Harness.TestEnvironment (TestEnvironment)
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Prelude
|
||||
import Test.Hspec (SpecWith, describe, it)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
spec :: SpecWith TestEnvironment
|
||||
spec =
|
||||
Fixture.runWithLocalTestEnvironment
|
||||
( NE.fromList
|
||||
[ (Fixture.fixture $ Fixture.Backend Fixture.DataConnectorMock)
|
||||
{ Fixture.mkLocalTestEnvironment = DataConnector.mkLocalTestEnvironmentMock,
|
||||
Fixture.setupTeardown = \(testEnv, mockEnv) ->
|
||||
[DataConnector.setupMockAction sourceMetadata DataConnector.mockBackendConfig (testEnv, mockEnv)]
|
||||
}
|
||||
]
|
||||
)
|
||||
tests
|
||||
|
||||
sourceMetadata :: Aeson.Value
|
||||
sourceMetadata =
|
||||
let source = defaultSource DataConnectorMock
|
||||
backendType = defaultBackendTypeString DataConnectorMock
|
||||
in [yaml|
|
||||
name : *source
|
||||
kind: *backendType
|
||||
tables:
|
||||
- table: [Album]
|
||||
configuration:
|
||||
custom_root_fields:
|
||||
select: albums
|
||||
select_by_pk: albums_by_pk
|
||||
column_config:
|
||||
AlbumId:
|
||||
custom_name: id
|
||||
Title:
|
||||
custom_name: title
|
||||
configuration: {}
|
||||
|]
|
||||
|
||||
tests :: Fixture.Options -> SpecWith (TestEnvironment, DataConnector.MockAgentEnvironment)
|
||||
tests opts = do
|
||||
describe "Error Protocol Tests" $ do
|
||||
it "handles returned errors correctly" $
|
||||
DataConnector.runMockedTest opts $
|
||||
let errorResponse = API.ErrorResponse API.UncaughtError "Hello World!" [yaml| { foo: "bar" } |]
|
||||
required =
|
||||
DataConnector.TestCaseRequired
|
||||
{ _givenRequired = DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Left errorResponse},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getAlbum {
|
||||
albums(limit: 1) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
|],
|
||||
_thenRequired =
|
||||
[yaml|
|
||||
errors:
|
||||
-
|
||||
extensions:
|
||||
code: "data-connector-error"
|
||||
path: "$"
|
||||
internal:
|
||||
foo: "bar"
|
||||
message: "UncaughtError: Hello World!"
|
||||
|]
|
||||
}
|
||||
in (DataConnector.defaultTestCase required)
|
||||
{ _whenQuery =
|
||||
Just
|
||||
( API.QueryRequest
|
||||
{ _qrTable = API.TableName ("Album" :| []),
|
||||
_qrTableRelationships = [],
|
||||
_qrQuery =
|
||||
API.Query
|
||||
{ _qFields =
|
||||
Just $
|
||||
HashMap.fromList
|
||||
[ (API.FieldName "id", API.ColumnField (API.ColumnName "AlbumId")),
|
||||
(API.FieldName "title", API.ColumnField (API.ColumnName "Title"))
|
||||
],
|
||||
_qAggregates = Nothing,
|
||||
_qLimit = Just 1,
|
||||
_qOffset = Nothing,
|
||||
_qWhere = Nothing,
|
||||
_qOrderBy = Nothing
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module Test.DataConnector.MockAgent.QueryRelationshipsSpec
|
||||
@ -145,7 +146,7 @@ tests opts = do
|
||||
)
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getTrack {
|
||||
@ -273,7 +274,7 @@ tests opts = do
|
||||
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Camarão que Dorme e Onda Leva")
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getTrack {
|
||||
@ -413,7 +414,7 @@ tests opts = do
|
||||
[ [ (API.FieldName "EmployeeId", API.mkColumnFieldValue $ Aeson.Number 3)
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getEmployee {
|
||||
|
@ -112,7 +112,7 @@ tests opts = do
|
||||
(API.FieldName "title", API.mkColumnFieldValue $ Aeson.String "For Those About To Rock We Salute You")
|
||||
]
|
||||
]
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> rowsResponse albums},
|
||||
in DataConnector.chinookMock {DataConnector._queryResponse = \_ -> Right (rowsResponse albums)},
|
||||
_whenRequestRequired =
|
||||
[graphql|
|
||||
query getAlbum {
|
||||
|
@ -76,6 +76,7 @@ library
|
||||
Hasura.Backends.DataConnector.API.V0.Capabilities
|
||||
Hasura.Backends.DataConnector.API.V0.Column
|
||||
Hasura.Backends.DataConnector.API.V0.ConfigSchema
|
||||
Hasura.Backends.DataConnector.API.V0.ErrorResponse
|
||||
Hasura.Backends.DataConnector.API.V0.Expression
|
||||
Hasura.Backends.DataConnector.API.V0.OrderBy
|
||||
Hasura.Backends.DataConnector.API.V0.Query
|
||||
@ -139,6 +140,7 @@ test-suite tests-dc-api
|
||||
Test.MetricsSpec
|
||||
Test.ExplainSpec
|
||||
Test.QuerySpec
|
||||
Test.ErrorSpec
|
||||
Test.QuerySpec.AggregatesSpec
|
||||
Test.QuerySpec.BasicSpec
|
||||
Test.QuerySpec.FilteringSpec
|
||||
|
@ -1,13 +1,21 @@
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
|
||||
--
|
||||
module Hasura.Backends.DataConnector.API
|
||||
( module V0,
|
||||
Api,
|
||||
CapabilitiesResponses,
|
||||
QueryResponses,
|
||||
SchemaApi,
|
||||
SchemaResponses,
|
||||
QueryApi,
|
||||
ConfigHeader,
|
||||
Prometheus,
|
||||
SourceNameHeader,
|
||||
SourceName,
|
||||
capabilitiesCase,
|
||||
schemaCase,
|
||||
queryCase,
|
||||
openApiSchema,
|
||||
Routes (..),
|
||||
apiClient,
|
||||
@ -25,29 +33,69 @@ import Hasura.Backends.DataConnector.API.V0 as V0
|
||||
import Network.HTTP.Media ((//), (/:))
|
||||
import Servant.API
|
||||
import Servant.API.Generic
|
||||
import Servant.Client (Client, ClientM, client)
|
||||
import Servant.Client (Client, ClientM, client, matchUnion)
|
||||
import Servant.OpenApi
|
||||
import Prelude (show)
|
||||
import Prelude (Maybe (Just, Nothing), Monad, show)
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Servant Routes
|
||||
|
||||
-- | This function defines a central place to ensure that all cases are covered for capabilities and error responses.
|
||||
-- When additional responses are added to the Union, this should be updated to ensure that all responses have been considered.
|
||||
-- A general function of this form doesn't seem easy to write currently as you need a type inequality with ErrorResponse.
|
||||
capabilitiesCase :: a -> (CapabilitiesResponse -> a) -> (ErrorResponse -> a) -> Union CapabilitiesResponses -> a
|
||||
capabilitiesCase defaultAction capabilitiesAction errorAction union = do
|
||||
let capabilitiesM = matchUnion @CapabilitiesResponse union
|
||||
let errorM = matchUnion @ErrorResponse union
|
||||
case (capabilitiesM, errorM) of
|
||||
(Just c, Nothing) -> capabilitiesAction c
|
||||
(Nothing, Just e) -> errorAction e
|
||||
_ -> defaultAction -- Note, this could technically include the (Just _, Just _) scenario which is not possible.
|
||||
|
||||
type CapabilitiesResponses = '[V0.CapabilitiesResponse, V0.ErrorResponse]
|
||||
|
||||
type CapabilitiesApi =
|
||||
"capabilities"
|
||||
:> Get '[JSON] V0.CapabilitiesResponse
|
||||
:> UVerb 'GET '[JSON] CapabilitiesResponses
|
||||
|
||||
-- | This function defines a central place to ensure that all cases are covered for schema and error responses.
|
||||
-- When additional responses are added to the Union, this should be updated to ensure that all responses have been considered.
|
||||
schemaCase :: Monad m => m a -> (SchemaResponse -> m a) -> (ErrorResponse -> m a) -> Union SchemaResponses -> m a
|
||||
schemaCase defaultAction schemaAction errorAction union = do
|
||||
let schemaM = matchUnion @SchemaResponse union
|
||||
let errorM = matchUnion @ErrorResponse union
|
||||
case (schemaM, errorM) of
|
||||
(Just c, Nothing) -> schemaAction c
|
||||
(Nothing, Just e) -> errorAction e
|
||||
_ -> defaultAction -- Note, this could technically include the (Just _, Just _) scenario which is not possible.
|
||||
|
||||
type SchemaResponses = '[V0.SchemaResponse, V0.ErrorResponse]
|
||||
|
||||
type SchemaApi =
|
||||
"schema"
|
||||
:> SourceNameHeader Required
|
||||
:> ConfigHeader Required
|
||||
:> Get '[JSON] V0.SchemaResponse
|
||||
:> UVerb 'GET '[JSON] SchemaResponses
|
||||
|
||||
-- | This function defines a central place to ensure that all cases are covered for query and error responses.
|
||||
-- When additional responses are added to the Union, this should be updated to ensure that all responses have been considered.
|
||||
queryCase :: Monad m => m a -> (QueryResponse -> m a) -> (ErrorResponse -> m a) -> Union QueryResponses -> m a
|
||||
queryCase defaultAction queryAction errorAction union = do
|
||||
let queryM = matchUnion @QueryResponse union
|
||||
let errorM = matchUnion @ErrorResponse union
|
||||
case (queryM, errorM) of
|
||||
(Just c, Nothing) -> queryAction c
|
||||
(Nothing, Just e) -> errorAction e
|
||||
_ -> defaultAction -- Note, this could technically include the (Just _, Just _) scenario which is not possible.
|
||||
|
||||
type QueryResponses = '[V0.QueryResponse, V0.ErrorResponse]
|
||||
|
||||
type QueryApi =
|
||||
"query"
|
||||
:> SourceNameHeader Required
|
||||
:> ConfigHeader Required
|
||||
:> ReqBody '[JSON] V0.QueryRequest
|
||||
:> Post '[JSON] V0.QueryResponse
|
||||
:> UVerb 'POST '[JSON] QueryResponses
|
||||
|
||||
type ExplainApi =
|
||||
"explain"
|
||||
|
@ -4,6 +4,7 @@ module Hasura.Backends.DataConnector.API.V0
|
||||
module Column,
|
||||
module ConfigSchema,
|
||||
module Expression,
|
||||
module ErrorResponse,
|
||||
module OrderBy,
|
||||
module Query,
|
||||
module Raw,
|
||||
@ -19,6 +20,7 @@ import Hasura.Backends.DataConnector.API.V0.Aggregate as Aggregate
|
||||
import Hasura.Backends.DataConnector.API.V0.Capabilities as Capabilities
|
||||
import Hasura.Backends.DataConnector.API.V0.Column as Column
|
||||
import Hasura.Backends.DataConnector.API.V0.ConfigSchema as ConfigSchema
|
||||
import Hasura.Backends.DataConnector.API.V0.ErrorResponse as ErrorResponse
|
||||
import Hasura.Backends.DataConnector.API.V0.Explain as Explain
|
||||
import Hasura.Backends.DataConnector.API.V0.Expression as Expression
|
||||
import Hasura.Backends.DataConnector.API.V0.OrderBy as OrderBy
|
||||
|
@ -33,6 +33,8 @@ import Autodocodec.OpenAPI ()
|
||||
import Control.DeepSeq (NFData)
|
||||
import Control.Monad ((<=<))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Text (encodeToLazyText)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Data (Data, Proxy (..))
|
||||
import Data.Foldable (toList)
|
||||
@ -43,7 +45,7 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
|
||||
import Data.Hashable (Hashable)
|
||||
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.OpenApi (NamedSchema (..), OpenApiType (OpenApiObject), Schema (..), ToSchema (..), declareSchemaRef)
|
||||
import Data.OpenApi (NamedSchema (..), OpenApiType (OpenApiObject, OpenApiString), Referenced (..), Schema (..), ToSchema (..), declareSchemaRef)
|
||||
import Data.Text (Text)
|
||||
import Data.Text qualified as Text
|
||||
import Data.Text.Lazy (toStrict)
|
||||
@ -53,6 +55,8 @@ import Hasura.Backends.DataConnector.API.V0.ConfigSchema (ConfigSchemaResponse)
|
||||
import Language.GraphQL.Draft.Parser qualified as GQL.Parser
|
||||
import Language.GraphQL.Draft.Printer qualified as GQL.Printer
|
||||
import Language.GraphQL.Draft.Syntax qualified as GQL.Syntax
|
||||
import Servant.API (HasStatus)
|
||||
import Servant.API.UVerb qualified as Servant
|
||||
import Prelude
|
||||
|
||||
-- | The 'Capabilities' describes the _capabilities_ of the
|
||||
@ -321,6 +325,9 @@ data CapabilitiesResponse = CapabilitiesResponse
|
||||
deriving stock (Eq, Show, Generic)
|
||||
deriving (FromJSON, ToJSON) via Autodocodec CapabilitiesResponse
|
||||
|
||||
instance Servant.HasStatus CapabilitiesResponse where
|
||||
type StatusOf CapabilitiesResponse = 200
|
||||
|
||||
instance HasCodec CapabilitiesResponse where
|
||||
codec =
|
||||
object "CapabilitiesResponse" $
|
||||
|
@ -0,0 +1,70 @@
|
||||
{-# LANGUAGE OverloadedLists #-}
|
||||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
||||
|
||||
module Hasura.Backends.DataConnector.API.V0.ErrorResponse
|
||||
( ErrorResponse (..),
|
||||
ErrorResponseType (..),
|
||||
errorResponseJsonText,
|
||||
errorResponseSummary,
|
||||
)
|
||||
where
|
||||
|
||||
import Autodocodec
|
||||
import Autodocodec.OpenAPI ()
|
||||
import Control.DeepSeq (NFData)
|
||||
import Control.Monad ((<=<))
|
||||
import Data.Aeson (FromJSON, ToJSON)
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Aeson.Text (encodeToLazyText)
|
||||
import Data.Bifunctor (first)
|
||||
import Data.Data (Data, Proxy (..))
|
||||
import Data.Foldable (toList)
|
||||
import Data.HashMap.Strict (HashMap)
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap)
|
||||
import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
|
||||
import Data.Hashable (Hashable)
|
||||
import Data.List.NonEmpty (NonEmpty, nonEmpty)
|
||||
import Data.Maybe (mapMaybe)
|
||||
import Data.OpenApi (NamedSchema (..), OpenApiType (OpenApiObject, OpenApiString), Referenced (..), Schema (..), ToSchema (..), declareSchemaRef)
|
||||
import Data.Text (Text, pack)
|
||||
import Data.Text.Lazy (toStrict)
|
||||
import GHC.Generics (Generic)
|
||||
import Servant.API (HasStatus)
|
||||
import Servant.API.UVerb qualified as Servant
|
||||
import Prelude
|
||||
|
||||
data ErrorResponseType
|
||||
= UncaughtError
|
||||
deriving stock (Eq, Show, Generic)
|
||||
|
||||
instance HasCodec ErrorResponseType where
|
||||
codec =
|
||||
named "ErrorResponseType" $
|
||||
stringConstCodec [(UncaughtError, "uncaught-error")]
|
||||
|
||||
data ErrorResponse = ErrorResponse
|
||||
{ _crType :: ErrorResponseType,
|
||||
_crMessage :: Text,
|
||||
_crDetails :: J.Value
|
||||
}
|
||||
deriving stock (Eq, Show, Generic)
|
||||
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec ErrorResponse
|
||||
|
||||
instance HasStatus ErrorResponse where
|
||||
type StatusOf ErrorResponse = 500
|
||||
|
||||
{-# HLINT ignore "Use tshow" #-}
|
||||
errorResponseSummary :: ErrorResponse -> Text
|
||||
errorResponseSummary ErrorResponse {..} = pack (show _crType) <> ": " <> _crMessage
|
||||
|
||||
errorResponseJsonText :: ErrorResponse -> Text
|
||||
errorResponseJsonText = toStrict . encodeToLazyText
|
||||
|
||||
instance HasCodec ErrorResponse where
|
||||
codec =
|
||||
object "ErrorResponse" $
|
||||
ErrorResponse
|
||||
<$> optionalFieldWithDefault "type" UncaughtError "Error type" .= _crType
|
||||
<*> requiredField "message" "Error message" .= _crMessage
|
||||
<*> optionalFieldWithDefault "details" J.Null "Error details" .= _crDetails
|
@ -52,6 +52,7 @@ import Hasura.Backends.DataConnector.API.V0.Expression qualified as API.V0
|
||||
import Hasura.Backends.DataConnector.API.V0.OrderBy qualified as API.V0
|
||||
import Hasura.Backends.DataConnector.API.V0.Relationships qualified as API.V0
|
||||
import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0
|
||||
import Servant.API (HasStatus (..))
|
||||
import Prelude
|
||||
|
||||
-- | A serializable request to retrieve strutured data from some
|
||||
@ -68,9 +69,12 @@ instance HasCodec QueryRequest where
|
||||
codec =
|
||||
object "QueryRequest" $
|
||||
QueryRequest
|
||||
<$> requiredField "table" "The name of the table to query" .= _qrTable
|
||||
<*> requiredField "table_relationships" "The relationships between tables involved in the entire query request" .= _qrTableRelationships
|
||||
<*> requiredField "query" "The details of the query against the table" .= _qrQuery
|
||||
<$> requiredField "table" "The name of the table to query"
|
||||
.= _qrTable
|
||||
<*> requiredField "table_relationships" "The relationships between tables involved in the entire query request"
|
||||
.= _qrTableRelationships
|
||||
<*> requiredField "query" "The details of the query against the table"
|
||||
.= _qrQuery
|
||||
|
||||
newtype FieldName = FieldName {unFieldName :: Text}
|
||||
deriving stock (Eq, Ord, Show, Generic, Data)
|
||||
@ -98,12 +102,18 @@ instance HasCodec Query where
|
||||
codec =
|
||||
named "Query" . object "Query" $
|
||||
Query
|
||||
<$> optionalFieldOrNull "fields" "Fields of the query" .= _qFields
|
||||
<*> optionalFieldOrNull "aggregates" "Aggregate fields of the query" .= _qAggregates
|
||||
<*> optionalFieldOrNull "limit" "Optionally limit to N results" .= _qLimit
|
||||
<*> optionalFieldOrNull "offset" "Optionally offset from the Nth result" .= _qOffset
|
||||
<*> optionalFieldOrNull "where" "Optionally constrain the results to satisfy some predicate" .= _qWhere
|
||||
<*> optionalFieldOrNull "order_by" "Optionally order the results by the value of one or more fields" .= _qOrderBy
|
||||
<$> optionalFieldOrNull "fields" "Fields of the query"
|
||||
.= _qFields
|
||||
<*> optionalFieldOrNull "aggregates" "Aggregate fields of the query"
|
||||
.= _qAggregates
|
||||
<*> optionalFieldOrNull "limit" "Optionally limit to N results"
|
||||
.= _qLimit
|
||||
<*> optionalFieldOrNull "offset" "Optionally offset from the Nth result"
|
||||
.= _qOffset
|
||||
<*> optionalFieldOrNull "where" "Optionally constrain the results to satisfy some predicate"
|
||||
.= _qWhere
|
||||
<*> optionalFieldOrNull "order_by" "Optionally order the results by the value of one or more fields"
|
||||
.= _qOrderBy
|
||||
|
||||
-- | A relationship consists of the following components:
|
||||
-- - a sub-query, from the perspective that a relationship field will occur
|
||||
@ -120,8 +130,10 @@ data RelationshipField = RelationshipField
|
||||
relationshipFieldObjectCodec :: JSONObjectCodec RelationshipField
|
||||
relationshipFieldObjectCodec =
|
||||
RelationshipField
|
||||
<$> requiredField "relationship" "The name of the relationship to follow for the subquery" .= _rfRelationship
|
||||
<*> requiredField "query" "Relationship query" .= _rfQuery
|
||||
<$> requiredField "relationship" "The name of the relationship to follow for the subquery"
|
||||
.= _rfRelationship
|
||||
<*> requiredField "query" "Relationship query"
|
||||
.= _rfQuery
|
||||
|
||||
-- | The specific fields that are targeted by a 'Query'.
|
||||
--
|
||||
@ -164,8 +176,13 @@ instance HasCodec QueryResponse where
|
||||
codec =
|
||||
named "QueryResponse" . object "QueryResponse" $
|
||||
QueryResponse
|
||||
<$> optionalFieldOrNull "rows" "The rows returned by the query, corresponding to the query's fields" .= _qrRows
|
||||
<*> optionalFieldOrNull "aggregates" "The results of the aggregates returned by the query" .= _qrAggregates
|
||||
<$> optionalFieldOrNull "rows" "The rows returned by the query, corresponding to the query's fields"
|
||||
.= _qrRows
|
||||
<*> optionalFieldOrNull "aggregates" "The results of the aggregates returned by the query"
|
||||
.= _qrAggregates
|
||||
|
||||
instance HasStatus QueryResponse where
|
||||
type StatusOf QueryResponse = 200
|
||||
|
||||
-- | FieldValue represents the value of a field in a 'QueryResponse', which in reality can
|
||||
-- be two things. One, a column field value which can be any JSON 'J.Value', or two, a
|
||||
|
@ -13,6 +13,7 @@ import Data.Hashable (Hashable)
|
||||
import Data.OpenApi (ToSchema)
|
||||
import GHC.Generics (Generic)
|
||||
import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0
|
||||
import Servant.API qualified as Servant
|
||||
import Prelude
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
@ -31,3 +32,6 @@ instance HasCodec SchemaResponse where
|
||||
codec =
|
||||
object "SchemaResponse" $
|
||||
SchemaResponse <$> requiredField "tables" "Available tables" .= _srTables
|
||||
|
||||
instance Servant.HasStatus SchemaResponse where
|
||||
type StatusOf SchemaResponse = 200
|
||||
|
@ -15,9 +15,10 @@ import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Backends.DataConnector.API.V0.Capabilities as API
|
||||
import Network.HTTP.Client (defaultManagerSettings, newManager)
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, ClientError, hoistClient, mkClientEnv, runClientM, (//))
|
||||
import Servant.Client (Client, ClientError, hoistClient, mkClientEnv, runClientM)
|
||||
import Test.CapabilitiesSpec qualified
|
||||
import Test.Data (TestData, mkTestData)
|
||||
import Test.Data (TestData, guardedCapabilities, mkTestData)
|
||||
import Test.ErrorSpec qualified
|
||||
import Test.ExplainSpec qualified
|
||||
import Test.HealthSpec qualified
|
||||
import Test.Hspec (Spec)
|
||||
@ -41,6 +42,7 @@ tests testData api sourceName agentConfig capabilities = do
|
||||
Test.CapabilitiesSpec.spec api agentConfig capabilities
|
||||
Test.SchemaSpec.spec testData api sourceName agentConfig capabilities
|
||||
Test.QuerySpec.spec testData api sourceName agentConfig capabilities
|
||||
Test.ErrorSpec.spec testData api sourceName agentConfig capabilities
|
||||
for_ (API._cMetrics capabilities) \m -> Test.MetricsSpec.spec api m
|
||||
for_ (API._cExplain capabilities) \_ -> Test.ExplainSpec.spec testData api sourceName agentConfig capabilities
|
||||
|
||||
@ -50,7 +52,7 @@ main = do
|
||||
case command of
|
||||
Test testOptions@TestOptions {..} -> do
|
||||
api <- mkIOApiClient testOptions
|
||||
agentCapabilities <- API._crCapabilities <$> (api // _capabilities)
|
||||
agentCapabilities <- API._crCapabilities <$> guardedCapabilities api
|
||||
let testData = mkTestData _toTestConfig
|
||||
let spec = tests testData api testSourceName _toAgentConfig agentCapabilities
|
||||
case _toExportMatchStrings of
|
||||
|
@ -1,6 +1,6 @@
|
||||
module Test.CapabilitiesSpec (spec) where
|
||||
|
||||
import Hasura.Backends.DataConnector.API (Capabilities, CapabilitiesResponse (..), Config, Routes (..), validateConfigAgainstConfigSchema)
|
||||
import Hasura.Backends.DataConnector.API (Capabilities, CapabilitiesResponse (..), Config, Routes (..), capabilitiesCase, validateConfigAgainstConfigSchema)
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Expectations (jsonShouldBe)
|
||||
@ -10,9 +10,13 @@ import Prelude
|
||||
spec :: Client IO (NamedRoutes Routes) -> Config -> Capabilities -> Spec
|
||||
spec api config expectedCapabilities = describe "capabilities API" $ do
|
||||
it "returns the expected capabilities" $ do
|
||||
CapabilitiesResponse capabilities _ <- api // _capabilities
|
||||
capabilities `jsonShouldBe` expectedCapabilities
|
||||
CapabilitiesResponse {..} <- capabilitiesGuard =<< (api // _capabilities)
|
||||
_crCapabilities `jsonShouldBe` expectedCapabilities
|
||||
|
||||
it "returns a schema that can be used to validate the current config" $ do
|
||||
CapabilitiesResponse _ configSchema <- api // _capabilities
|
||||
validateConfigAgainstConfigSchema configSchema config `jsonShouldBe` []
|
||||
CapabilitiesResponse {..} <- capabilitiesGuard =<< (api // _capabilities)
|
||||
validateConfigAgainstConfigSchema _crConfigSchemaResponse config `jsonShouldBe` []
|
||||
where
|
||||
defaultAction = fail "Unexpected data connector capabilities response - Unexpected Type"
|
||||
errorAction e = fail $ "Unexpected data connector capabilities error response: " <> show e
|
||||
capabilitiesGuard = capabilitiesCase defaultAction (pure) errorAction
|
||||
|
@ -24,6 +24,12 @@ module Test.Data
|
||||
_ColumnFieldString,
|
||||
_ColumnFieldBoolean,
|
||||
orderByColumn,
|
||||
guardQueryResponse,
|
||||
guardedQuery,
|
||||
guardErrorResponse,
|
||||
guardedCapabilities,
|
||||
guardCapabilitiesResponse,
|
||||
errorQuery,
|
||||
)
|
||||
where
|
||||
|
||||
@ -50,7 +56,11 @@ import Data.Scientific (Scientific)
|
||||
import Data.Text (Text)
|
||||
import Data.Text qualified as Text
|
||||
import Data.Text.Encoding qualified as Text
|
||||
import Hasura.Backends.DataConnector.API (capabilitiesCase, queryCase)
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Servant.API qualified as Servant
|
||||
import Servant.Client ((//))
|
||||
import Servant.Client qualified as Servant
|
||||
import Text.XML qualified as XML
|
||||
import Text.XML.Lens qualified as XML
|
||||
import Prelude
|
||||
@ -481,3 +491,33 @@ currentComparisonColumn columnName = API.ComparisonColumn API.CurrentTable $ API
|
||||
orderByColumn :: [API.RelationshipName] -> Text -> API.OrderDirection -> API.OrderByElement
|
||||
orderByColumn targetPath columnName orderDirection =
|
||||
API.OrderByElement targetPath (API.OrderByColumn $ API.ColumnName columnName) orderDirection
|
||||
|
||||
guardQueryResponse :: Servant.Union API.QueryResponses -> IO API.QueryResponse
|
||||
guardQueryResponse = queryCase defaultAction successAction errorAction
|
||||
where
|
||||
defaultAction = fail "Expected QueryResponse"
|
||||
successAction q = pure q
|
||||
errorAction e = fail $ "Expected QueryResponse, got " <> show e
|
||||
|
||||
guardedQuery :: Servant.Client IO (Servant.NamedRoutes API.Routes) -> API.SourceName -> API.Config -> API.QueryRequest -> IO API.QueryResponse
|
||||
guardedQuery api sourceName config queryRequest = guardQueryResponse =<< (api // API._query) sourceName config queryRequest
|
||||
|
||||
guardedCapabilities :: Servant.Client IO (Servant.NamedRoutes API.Routes) -> IO API.CapabilitiesResponse
|
||||
guardedCapabilities api = guardCapabilitiesResponse =<< (api // API._capabilities)
|
||||
|
||||
guardCapabilitiesResponse :: Servant.Union API.CapabilitiesResponses -> IO API.CapabilitiesResponse
|
||||
guardCapabilitiesResponse = capabilitiesCase defaultAction successAction errorAction
|
||||
where
|
||||
defaultAction = fail "Expected QueryResponse"
|
||||
successAction c = pure c
|
||||
errorAction e = fail $ "Expected QueryResponse, got " <> show e
|
||||
|
||||
guardErrorResponse :: Servant.Union API.QueryResponses -> IO API.ErrorResponse
|
||||
guardErrorResponse = queryCase defaultAction successAction errorAction
|
||||
where
|
||||
defaultAction = fail "Expected ErrorResponse"
|
||||
successAction q = fail $ "Expected ErrorResponse, got " <> show q
|
||||
errorAction e = pure e
|
||||
|
||||
errorQuery :: Servant.Client IO (Servant.NamedRoutes API.Routes) -> API.SourceName -> API.Config -> API.QueryRequest -> IO API.ErrorResponse
|
||||
errorQuery api sourceName config queryRequest = guardErrorResponse =<< (api // API._query) sourceName config queryRequest
|
||||
|
30
server/lib/dc-api/test/Test/ErrorSpec.hs
Normal file
30
server/lib/dc-api/test/Test/ErrorSpec.hs
Normal file
@ -0,0 +1,30 @@
|
||||
module Test.ErrorSpec (spec) where
|
||||
|
||||
import Control.Lens ((&), (?~))
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), errorQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Hspec (Spec, describe, it, shouldBe)
|
||||
import Prelude
|
||||
|
||||
spec :: TestData -> Client IO (NamedRoutes Routes) -> SourceName -> Config -> a -> Spec
|
||||
spec TestData {..} api sourceName config _capabilities = describe "Basic Queries" do
|
||||
describe "Error Protocol" do
|
||||
it "returns a structured error when sending an invalid query" do
|
||||
receivedArtistsError <- errorQuery api sourceName config brokenQueryRequest
|
||||
_crType receivedArtistsError `shouldBe` UncaughtError
|
||||
where
|
||||
brokenQueryRequest :: QueryRequest
|
||||
brokenQueryRequest =
|
||||
let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField "ArtistId"), ("Name", _tdColumnField "Name")]
|
||||
query =
|
||||
Data.emptyQuery
|
||||
& qFields ?~ fields
|
||||
& qWhere
|
||||
?~ ApplyBinaryComparisonOperator
|
||||
(CustomBinaryComparisonOperator "FOOBAR")
|
||||
(ComparisonColumn CurrentTable (ColumnName "ArtistId"))
|
||||
(ScalarValue "1")
|
||||
in QueryRequest _tdArtistsTableName [] query
|
@ -13,8 +13,8 @@ import Data.Maybe (fromMaybe, isJust, mapMaybe)
|
||||
import Data.Ord (Down (..))
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data (TestData (..))
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), guardedQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
@ -26,7 +26,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
it "counts all rows" $ do
|
||||
let aggregates = Data.mkFieldsMap [("count_all", StarCount)]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount = length _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)]
|
||||
@ -38,7 +38,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let where' = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "BillingCity") (ScalarValue (String "Oslo"))
|
||||
let aggregates = Data.mkFieldsMap [("count_all", StarCount)]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery . qWhere ?~ where'
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount = length $ filter ((^? Data.field "BillingCity" . Data._ColumnFieldString) >>> (== Just "Oslo")) _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)]
|
||||
@ -49,7 +49,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
it "counts all rows, after applying pagination" $ do
|
||||
let aggregates = Data.mkFieldsMap [("count_all", StarCount)]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ 20 >>> qOffset ?~ 400)
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount = length . take 20 $ drop 400 _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("count_all", Number $ fromIntegral invoiceCount)]
|
||||
@ -61,7 +61,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
it "counts all rows with non-null columns" $ do
|
||||
let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") False)]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount = length $ filter ((^? Data.field "BillingState" . Data._ColumnFieldString) >>> (/= Nothing)) _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("count_cols", Number $ fromIntegral invoiceCount)]
|
||||
@ -73,7 +73,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let where' = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "InvoiceId") (ScalarValue (Number 380))
|
||||
let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") False)]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ 20 >>> qWhere ?~ where')
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount =
|
||||
_tdInvoicesRows
|
||||
@ -90,7 +90,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
it "can count all rows with distinct non-null values in a column" $ do
|
||||
let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") True)]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let billingStateCount = length . HashSet.fromList $ mapMaybe ((^? Data.field "BillingState" . Data._ColumnFieldString)) _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("count_cols", Number $ fromIntegral billingStateCount)]
|
||||
@ -102,7 +102,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let where' = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "InvoiceId") (ScalarValue (Number 380))
|
||||
let aggregates = Data.mkFieldsMap [("count_cols", ColumnCount $ ColumnCountAggregate (_tdColumnName "BillingState") True)]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ 20 >>> qWhere ?~ where')
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let billingStateCount =
|
||||
_tdInvoicesRows
|
||||
@ -121,7 +121,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
it "can get the max total from all rows" $ do
|
||||
let aggregates = Data.mkFieldsMap [("max", SingleColumn $ SingleColumnAggregate Max (_tdColumnName "Total"))]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let maxTotal = maximum $ mapMaybe ((^? Data.field "Total" . Data._ColumnFieldNumber)) _tdInvoicesRows
|
||||
let expectedAggregates = Data.mkFieldsMap [("max", Number maxTotal)]
|
||||
@ -134,7 +134,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let orderBy = OrderBy mempty $ _tdOrderByColumn [] "BillingPostalCode" Descending :| [_tdOrderByColumn [] "InvoiceId" Ascending]
|
||||
let aggregates = Data.mkFieldsMap [("max", SingleColumn $ SingleColumnAggregate Max (_tdColumnName "Total"))]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qLimit ?~ 20 >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy)
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let maxTotal =
|
||||
_tdInvoicesRows
|
||||
@ -156,7 +156,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
("max", SingleColumn $ SingleColumnAggregate Max (_tdColumnName "Name"))
|
||||
]
|
||||
let queryRequest = artistsQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let names = mapMaybe ((^? Data.field "Name" . Data._ColumnFieldString)) _tdArtistsRows
|
||||
let expectedAggregates =
|
||||
@ -172,7 +172,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let where' = ApplyBinaryComparisonOperator LessThan (_tdCurrentComparisonColumn "ArtistId") (ScalarValue (Number 0))
|
||||
let aggregates = Data.mkFieldsMap [("min", SingleColumn $ SingleColumnAggregate Min (_tdColumnName "Name"))]
|
||||
let queryRequest = artistsQueryRequest aggregates & qrQuery . qWhere ?~ where'
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let expectedAggregates = Data.mkFieldsMap [("min", Null)]
|
||||
|
||||
@ -188,7 +188,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
("maxTotal", SingleColumn $ SingleColumnAggregate Max (_tdColumnName "Total"))
|
||||
]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceCount = length _tdInvoicesRows
|
||||
let billingStateCount = length . HashSet.fromList $ mapMaybe ((^? Data.field "BillingState" . Data._ColumnFieldString)) _tdInvoicesRows
|
||||
@ -211,7 +211,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
("minTotal", SingleColumn $ SingleColumnAggregate Min (_tdColumnName "Total"))
|
||||
]
|
||||
let queryRequest = invoicesQueryRequest aggregates
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let maxInvoiceId = aggregate (Number . minimum) $ mapMaybe ((^? Data.field "InvoiceId" . Data._ColumnFieldNumber)) _tdInvoicesRows
|
||||
let maxTotal = aggregate (Number . minimum) $ mapMaybe ((^? Data.field "Total" . Data._ColumnFieldNumber)) _tdInvoicesRows
|
||||
@ -235,7 +235,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
let orderBy = OrderBy mempty $ _tdOrderByColumn [] "BillingAddress" Ascending :| [_tdOrderByColumn [] "InvoiceId" Ascending]
|
||||
let aggregates = Data.mkFieldsMap [("min", SingleColumn $ SingleColumnAggregate Min (_tdColumnName "Total"))]
|
||||
let queryRequest = invoicesQueryRequest aggregates & qrQuery %~ (qFields ?~ fields >>> qLimit ?~ 30 >>> qWhere ?~ where' >>> qOrderBy ?~ orderBy)
|
||||
response <- (api // _query) sourceName config queryRequest
|
||||
response <- guardedQuery api sourceName config queryRequest
|
||||
|
||||
let invoiceRows =
|
||||
_tdInvoicesRows
|
||||
@ -258,7 +258,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
describe "Aggregates via Relationships" $ do
|
||||
it "can query aggregates via an array relationship" $ do
|
||||
let query = artistsWithAlbumsQuery id & qrQuery . qLimit ?~ 5
|
||||
receivedArtists <- (api // _query) sourceName config query
|
||||
receivedArtists <- guardedQuery api sourceName config query
|
||||
|
||||
let joinInAlbums (artist :: HashMap FieldName FieldValue) = fromMaybe artist $ do
|
||||
artistId <- artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -283,7 +283,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
("Title", _tdColumnField "Title")
|
||||
]
|
||||
let query = artistsWithAlbumsQuery (qFields ?~ albumFields) & qrQuery . qLimit ?~ 5
|
||||
receivedArtists <- (api // _query) sourceName config query
|
||||
receivedArtists <- guardedQuery api sourceName config query
|
||||
|
||||
let joinInAlbums (artist :: HashMap FieldName FieldValue) = fromMaybe artist $ do
|
||||
artistId <- artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -303,7 +303,7 @@ spec TestData {..} api sourceName config relationshipCapabilities = describe "Ag
|
||||
Data.responseAggregates receivedArtists `jsonShouldBe` mempty
|
||||
|
||||
it "can query with many nested relationships, with aggregates at multiple levels, with filtering, pagination and ordering" $ do
|
||||
receivedArtists <- (api // _query) sourceName config deeplyNestedArtistsQuery
|
||||
receivedArtists <- guardedQuery api sourceName config deeplyNestedArtistsQuery
|
||||
|
||||
let joinInMediaType (track :: HashMap FieldName FieldValue) = fromMaybe track $ do
|
||||
mediaTypeId <- track ^? Data.field "MediaTypeId" . Data._ColumnFieldNumber
|
||||
|
@ -5,8 +5,8 @@ import Control.Lens ((%~), (&), (?~))
|
||||
import Data.HashMap.Strict qualified as HashMap
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data (TestData (..))
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), guardedQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
@ -17,7 +17,7 @@ spec TestData {..} api sourceName config = describe "Basic Queries" $ do
|
||||
describe "Column Fields" $ do
|
||||
it "can query for a list of artists" $ do
|
||||
let query = artistsQueryRequest
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedArtists = _tdArtistsRows
|
||||
Data.responseRows receivedArtists `rowsShouldBe` expectedArtists
|
||||
@ -26,7 +26,7 @@ spec TestData {..} api sourceName config = describe "Basic Queries" $ do
|
||||
it "can query for a list of albums with a subset of columns" $ do
|
||||
let fields = Data.mkFieldsMap [("ArtistId", _tdColumnField "ArtistId"), ("Title", _tdColumnField "Title")]
|
||||
let query = albumsQueryRequest & qrQuery . qFields ?~ fields
|
||||
receivedAlbums <- Data.sortResponseRowsBy "Title" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "Title" <$> guardedQuery api sourceName config query
|
||||
|
||||
let filterToRequiredProperties =
|
||||
HashMap.filterWithKey (\(FieldName propName) _value -> propName == "ArtistId" || propName == "Title")
|
||||
@ -38,7 +38,7 @@ spec TestData {..} api sourceName config = describe "Basic Queries" $ do
|
||||
it "can project columns into fields with different names" $ do
|
||||
let fields = Data.mkFieldsMap [("Artist_Id", _tdColumnField "ArtistId"), ("Artist_Name", _tdColumnField "Name")]
|
||||
let query = artistsQueryRequest & qrQuery . qFields ?~ fields
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let renameProperties =
|
||||
HashMap.mapKeys
|
||||
@ -58,9 +58,9 @@ spec TestData {..} api sourceName config = describe "Basic Queries" $ do
|
||||
let page1Query = artistsQueryRequest & qrQuery %~ (qLimit ?~ 10 >>> qOffset ?~ 0)
|
||||
let page2Query = artistsQueryRequest & qrQuery %~ (qLimit ?~ 10 >>> qOffset ?~ 10)
|
||||
|
||||
allArtists <- Data.responseRows <$> (api // _query) sourceName config allQuery
|
||||
page1Artists <- Data.responseRows <$> (api // _query) sourceName config page1Query
|
||||
page2Artists <- Data.responseRows <$> (api // _query) sourceName config page2Query
|
||||
allArtists <- Data.responseRows <$> guardedQuery api sourceName config allQuery
|
||||
page1Artists <- Data.responseRows <$> guardedQuery api sourceName config page1Query
|
||||
page2Artists <- Data.responseRows <$> guardedQuery api sourceName config page2Query
|
||||
|
||||
page1Artists `rowsShouldBe` take 10 allArtists
|
||||
page2Artists `rowsShouldBe` take 10 (drop 10 allArtists)
|
||||
|
@ -10,8 +10,8 @@ import Data.List (sortOn)
|
||||
import Data.Maybe (isJust, mapMaybe)
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data (TestData (..))
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), guardedQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
@ -22,7 +22,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter using an equality expression" $ do
|
||||
let where' = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 2))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((== Just 2) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -33,7 +33,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter using an inequality expression" $ do
|
||||
let where' = Not (ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 2)))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((/= Just 2) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -44,7 +44,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter using an in expression" $ do
|
||||
let where' = ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "AlbumId") [Number 2, Number 3]
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip elem [Just 2, Just 3] . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -55,7 +55,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can negate an in expression filter using a not expression" $ do
|
||||
let where' = Not (ApplyBinaryArrayComparisonOperator In (_tdCurrentComparisonColumn "AlbumId") [Number 2, Number 3])
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip notElem [Just 2, Just 3] . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -68,7 +68,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
let where2 = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "Title") (ScalarValue (String "Stormbringer"))
|
||||
let where' = And [where1, where2]
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter
|
||||
@ -83,7 +83,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "treats an empty and expression as 'true'" $ do
|
||||
let where' = And []
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` _tdAlbumsRows
|
||||
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
||||
@ -93,7 +93,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
let where2 = ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 3))
|
||||
let where' = Or [where1, where2]
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (flip elem [Just 2, Just 3] . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -104,7 +104,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "treats an empty or expression as 'false'" $ do
|
||||
let where' = Or []
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` []
|
||||
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
||||
@ -112,7 +112,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter by applying the greater than operator" $ do
|
||||
let where' = ApplyBinaryComparisonOperator GreaterThan (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 300))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((> Just 300) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -123,7 +123,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter by applying the greater than or equal operator" $ do
|
||||
let where' = ApplyBinaryComparisonOperator GreaterThanOrEqual (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 300))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((>= Just 300) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -134,7 +134,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter by applying the less than operator" $ do
|
||||
let where' = ApplyBinaryComparisonOperator LessThan (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 100))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((< Just 100) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -145,7 +145,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter by applying the less than or equal operator" $ do
|
||||
let where' = ApplyBinaryComparisonOperator LessThanOrEqual (_tdCurrentComparisonColumn "AlbumId") (ScalarValue (Number 100))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter ((<= Just 100) . (^? Data.field "AlbumId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -156,7 +156,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
it "can filter using a greater than operator with a column comparison" $ do
|
||||
let where' = ApplyBinaryComparisonOperator GreaterThan (_tdCurrentComparisonColumn "AlbumId") (AnotherColumn (_tdCurrentComparisonColumn "ArtistId"))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
filter (\album -> (album ^? Data.field "AlbumId" . Data._ColumnFieldNumber) > (album ^? Data.field "ArtistId" . Data._ColumnFieldNumber)) _tdAlbumsRows
|
||||
@ -173,7 +173,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
Exists (UnrelatedTable _tdEmployeesTableName) $
|
||||
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "EmployeeId") (ScalarValue (Number 1))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums = _tdAlbumsRows
|
||||
|
||||
@ -185,7 +185,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
Exists (UnrelatedTable _tdEmployeesTableName) $
|
||||
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "EmployeeId") (ScalarValue (Number 0))
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` []
|
||||
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
||||
@ -199,7 +199,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "City") (ScalarValue (String "Edmonton"))
|
||||
]
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums = _tdAlbumsRows
|
||||
|
||||
@ -214,7 +214,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
ApplyBinaryComparisonOperator Equal (_tdCurrentComparisonColumn "City") (ScalarValue (String "Calgary"))
|
||||
]
|
||||
let query = albumsQueryRequest & qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` []
|
||||
_qrAggregates receivedAlbums `jsonShouldBe` Nothing
|
||||
@ -229,7 +229,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
albumsQueryRequest
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships]
|
||||
& qrQuery . qWhere ?~ where'
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let artistId =
|
||||
_tdArtistsRows
|
||||
@ -256,7 +256,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
Data.onlyKeepRelationships [_tdGenreRelationshipName] _tdTracksTableRelationships
|
||||
]
|
||||
& qrQuery . qWhere ?~ where'
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let genreId =
|
||||
_tdGenresRows
|
||||
@ -292,7 +292,7 @@ spec TestData {..} api sourceName config comparisonCapabilities = describe "Filt
|
||||
artistsQueryRequest
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]
|
||||
& qrQuery . qWhere ?~ where'
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let albums =
|
||||
_tdAlbumsRows
|
||||
|
@ -13,8 +13,8 @@ import Data.Maybe (isJust)
|
||||
import Data.Ord (Down (..))
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data (TestData (..))
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), guardedQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
@ -25,7 +25,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
it "can order results in ascending order" $ do
|
||||
let orderBy = OrderBy mempty $ _tdOrderByColumn [] "Title" Ascending :| []
|
||||
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums = sortOn (^? Data.field "Title") _tdAlbumsRows
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
|
||||
@ -34,7 +34,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
it "can order results in descending order" $ do
|
||||
let orderBy = OrderBy mempty $ _tdOrderByColumn [] "Title" Descending :| []
|
||||
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums = sortOn (Down . (^? Data.field "Title")) _tdAlbumsRows
|
||||
Data.responseRows receivedAlbums `rowsShouldBe` expectedAlbums
|
||||
@ -43,7 +43,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
it "can use multiple order by elements to order results" $ do
|
||||
let orderBy = OrderBy mempty $ _tdOrderByColumn [] "ArtistId" Ascending :| [_tdOrderByColumn [] "Title" Descending]
|
||||
let query = albumsQueryRequest & qrQuery . qOrderBy ?~ orderBy
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let expectedAlbums =
|
||||
sortOn (\album -> (album ^? Data.field "ArtistId", Down (album ^? Data.field "Title"))) _tdAlbumsRows
|
||||
@ -59,7 +59,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
albumsQueryRequest
|
||||
& qrQuery . qOrderBy ?~ orderBy
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships]
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let getRelatedArtist (album :: HashMap FieldName FieldValue) =
|
||||
(album ^? Data.field "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> _tdArtistsRowsById ^? ix artistId
|
||||
@ -81,7 +81,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
albumsQueryRequest
|
||||
& qrQuery . qOrderBy ?~ orderBy
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships]
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let getRelatedArtist (album :: HashMap FieldName FieldValue) = do
|
||||
artist <- (album ^? Data.field "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> _tdArtistsRowsById ^? ix artistId
|
||||
@ -127,7 +127,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
.~ [ Data.onlyKeepRelationships [_tdAlbumRelationshipName] _tdTracksTableRelationships,
|
||||
Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships
|
||||
]
|
||||
receivedTracks <- (api // _query) sourceName config query
|
||||
receivedTracks <- guardedQuery api sourceName config query
|
||||
|
||||
let getRelatedArtist (track :: HashMap FieldName FieldValue) = do
|
||||
albumId <- track ^? Data.field "AlbumId" . Data._ColumnFieldNumber
|
||||
@ -151,7 +151,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
artistsQueryRequest
|
||||
& qrQuery . qOrderBy ?~ orderBy
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]
|
||||
receivedArtists <- (api // _query) sourceName config query
|
||||
receivedArtists <- guardedQuery api sourceName config query
|
||||
|
||||
let getAlbumsCount (artist :: HashMap FieldName FieldValue) = do
|
||||
artistId <- artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -175,7 +175,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
artistsQueryRequest
|
||||
& qrQuery . qOrderBy ?~ orderBy
|
||||
& qrTableRelationships .~ [Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships]
|
||||
receivedArtists <- (api // _query) sourceName config query
|
||||
receivedArtists <- guardedQuery api sourceName config query
|
||||
|
||||
let getAlbumsCount (artist :: HashMap FieldName FieldValue) = do
|
||||
artistId <- artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -220,7 +220,7 @@ spec TestData {..} api sourceName config Capabilities {..} = describe "Order By
|
||||
.~ [ Data.onlyKeepRelationships [_tdArtistRelationshipName] _tdAlbumsTableRelationships,
|
||||
Data.onlyKeepRelationships [_tdAlbumsRelationshipName] _tdArtistsTableRelationships
|
||||
]
|
||||
receivedAlbums <- (api // _query) sourceName config query
|
||||
receivedAlbums <- guardedQuery api sourceName config query
|
||||
|
||||
let getTotalArtistAlbumsCount (album :: HashMap FieldName FieldValue) = do
|
||||
artistId <- album ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
|
@ -10,8 +10,8 @@ import Data.List.NonEmpty qualified as NonEmpty
|
||||
import Data.Maybe (maybeToList)
|
||||
import Hasura.Backends.DataConnector.API
|
||||
import Servant.API (NamedRoutes)
|
||||
import Servant.Client (Client, (//))
|
||||
import Test.Data (TestData (..))
|
||||
import Servant.Client (Client)
|
||||
import Test.Data (TestData (..), guardedQuery)
|
||||
import Test.Data qualified as Data
|
||||
import Test.Expectations (jsonShouldBe, rowsShouldBe)
|
||||
import Test.Hspec (Spec, describe, it)
|
||||
@ -21,7 +21,7 @@ spec :: TestData -> Client IO (NamedRoutes Routes) -> SourceName -> Config -> Ma
|
||||
spec TestData {..} api sourceName config subqueryComparisonCapabilities = describe "Relationship Queries" $ do
|
||||
it "perform an object relationship query by joining artist to albums" $ do
|
||||
let query = albumsWithArtistQuery id
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> (api // _query) sourceName config query
|
||||
receivedAlbums <- Data.sortResponseRowsBy "AlbumId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInArtist (album :: HashMap FieldName FieldValue) =
|
||||
let artist = (album ^? Data.field "ArtistId" . Data._ColumnFieldNumber) >>= \artistId -> _tdArtistsRowsById ^? ix artistId
|
||||
@ -35,7 +35,7 @@ spec TestData {..} api sourceName config subqueryComparisonCapabilities = descri
|
||||
|
||||
it "perform an array relationship query by joining albums to artists" $ do
|
||||
let query = artistsWithAlbumsQuery id
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInAlbums (artist :: HashMap FieldName FieldValue) =
|
||||
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -51,7 +51,7 @@ spec TestData {..} api sourceName config subqueryComparisonCapabilities = descri
|
||||
it "perform an array relationship query by joining albums to artists with pagination of albums" $ do
|
||||
let albumsOrdering = OrderBy mempty $ NonEmpty.fromList [_tdOrderByColumn [] "AlbumId" Ascending]
|
||||
let query = artistsWithAlbumsQuery (qOffset ?~ 1 >>> qLimit ?~ 2 >>> qOrderBy ?~ albumsOrdering)
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> (api // _query) sourceName config query
|
||||
receivedArtists <- Data.sortResponseRowsBy "ArtistId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInAlbums (artist :: HashMap FieldName FieldValue) = do
|
||||
let artistId = artist ^? Data.field "ArtistId" . Data._ColumnFieldNumber
|
||||
@ -79,7 +79,7 @@ spec TestData {..} api sourceName config subqueryComparisonCapabilities = descri
|
||||
(_tdCurrentComparisonColumn "Country")
|
||||
(AnotherColumn (_tdQueryComparisonColumn "Country"))
|
||||
let query = customersWithSupportRepQuery id & qrQuery . qWhere ?~ where'
|
||||
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> (api // _query) sourceName config query
|
||||
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInSupportRep (customer :: HashMap FieldName FieldValue) =
|
||||
let supportRep = (customer ^? Data.field "SupportRepId" . Data._ColumnFieldNumber) >>= \employeeId -> _tdEmployeesRowsById ^? ix employeeId
|
||||
@ -107,7 +107,7 @@ spec TestData {..} api sourceName config subqueryComparisonCapabilities = descri
|
||||
(_tdCurrentComparisonColumn "Country")
|
||||
(AnotherColumn (_tdQueryComparisonColumn "Country"))
|
||||
let query = employeesWithCustomersQuery id & qrQuery . qWhere ?~ where'
|
||||
receivedEmployees <- Data.sortResponseRowsBy "EmployeeId" <$> (api // _query) sourceName config query
|
||||
receivedEmployees <- Data.sortResponseRowsBy "EmployeeId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInCustomers (employee :: HashMap FieldName FieldValue) =
|
||||
let employeeId = employee ^? Data.field "EmployeeId" . Data._ColumnFieldNumber
|
||||
@ -148,7 +148,7 @@ spec TestData {..} api sourceName config subqueryComparisonCapabilities = descri
|
||||
(AnotherColumn (_tdCurrentComparisonColumn "LastName"))
|
||||
|
||||
let query = customersWithSupportRepQuery (\q -> q & qWhere ?~ employeesWhere) & qrQuery . qWhere ?~ customersWhere
|
||||
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> (api // _query) sourceName config query
|
||||
receivedCustomers <- Data.sortResponseRowsBy "CustomerId" <$> guardedQuery api sourceName config query
|
||||
|
||||
let joinInSupportRep (customer :: HashMap FieldName FieldValue) =
|
||||
let supportRep = do
|
||||
|
@ -27,13 +27,13 @@ spec :: TestData -> Client IO (NamedRoutes API.Routes) -> API.SourceName -> API.
|
||||
spec TestData {..} api sourceName config API.Capabilities {..} = describe "schema API" $ do
|
||||
it "returns the Chinook tables" $ do
|
||||
let extractTableNames = sort . fmap API._tiName
|
||||
tableNames <- (extractTableNames . API._srTables) <$> (api // API._schema) sourceName config
|
||||
tableNames <- (extractTableNames . API._srTables) <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
|
||||
let expectedTableNames = extractTableNames _tdSchemaTables
|
||||
tableNames `jsonShouldBe` expectedTableNames
|
||||
|
||||
testPerTable "returns the correct columns in the Chinook tables" $ \expectedTable@API.TableInfo {..} -> do
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (api // API._schema) sourceName config
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
|
||||
-- We remove some properties here so that we don't compare them since they vary between agent implementations
|
||||
let extractJsonForComparison table =
|
||||
@ -54,17 +54,17 @@ spec TestData {..} api sourceName config API.Capabilities {..} = describe "schem
|
||||
|
||||
if API._dscSupportsPrimaryKeys _cDataSchema
|
||||
then testPerTable "returns the correct primary keys for the Chinook tables" $ \API.TableInfo {..} -> do
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (api // API._schema) sourceName config
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
let actualPrimaryKey = API._tiPrimaryKey <$> tables
|
||||
actualPrimaryKey `jsonShouldBe` Just _tiPrimaryKey
|
||||
else testPerTable "returns no primary keys for the Chinook tables" $ \API.TableInfo {..} -> do
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (api // API._schema) sourceName config
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
let actualPrimaryKey = API._tiPrimaryKey <$> tables
|
||||
actualPrimaryKey `jsonShouldBe` Just []
|
||||
|
||||
if API._dscSupportsForeignKeys _cDataSchema
|
||||
then testPerTable "returns the correct foreign keys for the Chinook tables" $ \expectedTable@API.TableInfo {..} -> do
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (api // API._schema) sourceName config
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
|
||||
-- We compare only the constraints and ignore the constraint names since some agents will have
|
||||
-- different constraint names
|
||||
@ -75,7 +75,7 @@ spec TestData {..} api sourceName config API.Capabilities {..} = describe "schem
|
||||
|
||||
actualConstraints `jsonShouldBe` expectedConstraints
|
||||
else testPerTable "returns no foreign keys for the Chinook tables" $ \API.TableInfo {..} -> do
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (api // API._schema) sourceName config
|
||||
tables <- find (\t -> API._tiName t == _tiName) . API._srTables <$> (schemaGuard =<< (api // API._schema) sourceName config)
|
||||
|
||||
let actualJsonConstraints = API._tiForeignKeys <$> tables
|
||||
actualJsonConstraints `jsonShouldBe` Just (API.ForeignKeys mempty)
|
||||
@ -86,3 +86,7 @@ spec TestData {..} api sourceName config API.Capabilities {..} = describe "schem
|
||||
forM_ _tdSchemaTables $ \expectedTable@API.TableInfo {..} -> do
|
||||
it (Text.unpack . NonEmpty.last $ API.unTableName _tiName) $
|
||||
test expectedTable
|
||||
|
||||
schemaGuard = API.schemaCase defaultAction pure errorAction
|
||||
defaultAction = fail "Error resolving source schema"
|
||||
errorAction e = fail ("Error resolving source schema: " <> Text.unpack (API.errorResponseJsonText e))
|
||||
|
@ -1,3 +1,5 @@
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
|
||||
module Harness.Backend.DataConnector.MockAgent
|
||||
( MockConfig (..),
|
||||
chinookMock,
|
||||
@ -11,6 +13,7 @@ import Data.HashMap.Strict.InsOrd qualified as HMap
|
||||
import Data.IORef qualified as I
|
||||
import Data.OpenApi qualified as OpenApi
|
||||
import Data.Proxy
|
||||
import Data.SOP.BasicFunctors qualified as SOP
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Prelude
|
||||
import Network.Wai.Handler.Warp qualified as Warp
|
||||
@ -18,10 +21,13 @@ import Servant
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Note: Only the _queryResponse field allows mock errors at present.
|
||||
-- This can be extended at a later point if required.
|
||||
--
|
||||
data MockConfig = MockConfig
|
||||
{ _capabilitiesResponse :: API.CapabilitiesResponse,
|
||||
_schemaResponse :: API.SchemaResponse,
|
||||
_queryResponse :: API.QueryRequest -> API.QueryResponse
|
||||
_queryResponse :: API.QueryRequest -> Either API.ErrorResponse API.QueryResponse
|
||||
}
|
||||
|
||||
mkTableName :: Text -> API.TableName
|
||||
@ -536,28 +542,30 @@ chinookMock =
|
||||
MockConfig
|
||||
{ _capabilitiesResponse = capabilities,
|
||||
_schemaResponse = schema,
|
||||
_queryResponse = \_ -> API.QueryResponse (Just []) Nothing
|
||||
_queryResponse = \_ -> Right $ API.QueryResponse (Just []) Nothing
|
||||
}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
mockCapabilitiesHandler :: I.IORef MockConfig -> Handler API.CapabilitiesResponse
|
||||
mockCapabilitiesHandler :: I.IORef MockConfig -> Handler (Union API.CapabilitiesResponses)
|
||||
mockCapabilitiesHandler mcfg = liftIO $ do
|
||||
cfg <- I.readIORef mcfg
|
||||
pure $ _capabilitiesResponse cfg
|
||||
pure $ inject $ SOP.I $ _capabilitiesResponse cfg
|
||||
|
||||
mockSchemaHandler :: I.IORef MockConfig -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> Handler API.SchemaResponse
|
||||
mockSchemaHandler :: I.IORef MockConfig -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> Handler (Union API.SchemaResponses)
|
||||
mockSchemaHandler mcfg mQueryConfig _sourceName queryConfig = liftIO $ do
|
||||
cfg <- I.readIORef mcfg
|
||||
I.writeIORef mQueryConfig (Just queryConfig)
|
||||
pure $ _schemaResponse cfg
|
||||
pure $ inject $ SOP.I $ _schemaResponse cfg
|
||||
|
||||
mockQueryHandler :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> API.QueryRequest -> Handler API.QueryResponse
|
||||
mockQueryHandler :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> API.QueryRequest -> Handler (Union API.QueryResponses)
|
||||
mockQueryHandler mcfg mquery mQueryCfg _sourceName queryConfig query = liftIO $ do
|
||||
handler <- fmap _queryResponse $ I.readIORef mcfg
|
||||
I.writeIORef mquery (Just query)
|
||||
I.writeIORef mQueryCfg (Just queryConfig)
|
||||
pure $ handler query
|
||||
case handler query of
|
||||
Left e -> pure $ inject $ SOP.I e
|
||||
Right r -> pure $ inject $ SOP.I r
|
||||
|
||||
-- Returns an empty explain response for now
|
||||
explainHandler :: API.SourceName -> API.Config -> API.QueryRequest -> Handler API.ExplainResponse
|
||||
|
@ -37,6 +37,7 @@ library
|
||||
, resourcet
|
||||
, safe-exceptions
|
||||
, servant-server
|
||||
, sop-core
|
||||
, template-haskell
|
||||
, text
|
||||
, th-lift
|
||||
|
@ -10,12 +10,14 @@ where
|
||||
import Data.Aeson qualified as J
|
||||
import Data.Environment qualified as Env
|
||||
import Data.Text.Extended (toTxt)
|
||||
import Hasura.Backends.DataConnector.API (errorResponseSummary, queryCase)
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Backends.DataConnector.API.V0.ErrorResponse (ErrorResponse (..))
|
||||
import Hasura.Backends.DataConnector.Adapter.ConfigTransform (transformSourceConfig)
|
||||
import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..))
|
||||
import Hasura.Backends.DataConnector.Agent.Client (AgentClientT)
|
||||
import Hasura.Backends.DataConnector.Plan qualified as DC
|
||||
import Hasura.Base.Error (Code (..), QErr, throw400, throw500)
|
||||
import Hasura.Base.Error (Code (..), QErr, throw400, throw400WithDetail, throw500)
|
||||
import Hasura.EncJSON (EncJSON, encJFromBuilder, encJFromJValue)
|
||||
import Hasura.GraphQL.Execute.Backend (BackendExecute (..), DBStepInfo (..), ExplainPlan (..))
|
||||
import Hasura.GraphQL.Namespace qualified as GQL
|
||||
@ -76,9 +78,14 @@ buildQueryAction sourceName SourceConfig {..} DC.QueryPlan {..} = do
|
||||
when (DC.queryHasRelations _qpRequest && isNothing (API._cRelationships _scCapabilities)) $
|
||||
throw400 NotSupported "Agents must provide their own dataloader."
|
||||
let apiQueryRequest = Witch.into @API.QueryRequest _qpRequest
|
||||
queryResponse <- (genericClient // API._query) (toTxt sourceName) _scConfig apiQueryRequest
|
||||
|
||||
queryResponse <- queryGuard =<< (genericClient // API._query) (toTxt sourceName) _scConfig apiQueryRequest
|
||||
reshapedResponse <- _qpResponseReshaper queryResponse
|
||||
pure . encJFromBuilder $ J.fromEncoding reshapedResponse
|
||||
where
|
||||
errorAction e = throw400WithDetail DataConnectorError (errorResponseSummary e) (_crDetails e)
|
||||
defaultAction = throw400 DataConnectorError "Unexpected data connector capabilities response - Unexpected Type"
|
||||
queryGuard = queryCase defaultAction pure errorAction
|
||||
|
||||
-- Delegates the generation to the Agent's /explain endpoint if it has that capability,
|
||||
-- otherwise, returns the IR sent to the agent.
|
||||
|
@ -17,12 +17,14 @@ import Data.Sequence qualified as Seq
|
||||
import Data.Sequence.NonEmpty qualified as NESeq
|
||||
import Data.Text qualified as Text
|
||||
import Data.Text.Extended (toTxt, (<<>), (<>>))
|
||||
import Hasura.Backends.DataConnector.API (capabilitiesCase, errorResponseSummary, schemaCase)
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Backends.DataConnector.API.V0.ErrorResponse (_crDetails)
|
||||
import Hasura.Backends.DataConnector.Adapter.ConfigTransform (transformConnSourceConfig)
|
||||
import Hasura.Backends.DataConnector.Adapter.Types qualified as DC
|
||||
import Hasura.Backends.DataConnector.Agent.Client (AgentClientContext (..), runAgentClientT)
|
||||
import Hasura.Backends.Postgres.SQL.Types (PGDescription (..))
|
||||
import Hasura.Base.Error (Code (..), QErr, decodeValue, throw400, throw500, withPathK)
|
||||
import Hasura.Base.Error (Code (..), QErr, decodeValue, throw400, throw400WithDetail, throw500, withPathK)
|
||||
import Hasura.Incremental qualified as Inc
|
||||
import Hasura.Logging (Hasura, Logger)
|
||||
import Hasura.Prelude
|
||||
@ -112,11 +114,15 @@ resolveBackendInfo' logger = proc (invalidationKeys, optionsMap) -> do
|
||||
HTTP.Manager ->
|
||||
m (Either QErr DC.DataConnectorInfo)
|
||||
getDataConnectorCapabilities options@DC.DataConnectorOptions {..} manager = runExceptT do
|
||||
API.CapabilitiesResponse {..} <-
|
||||
capabilitiesU <-
|
||||
runTraceTWithReporter noReporter "capabilities"
|
||||
. flip runAgentClientT (AgentClientContext logger _dcoUri manager Nothing)
|
||||
$ genericClient // API._capabilities
|
||||
return $ DC.DataConnectorInfo options _crCapabilities _crConfigSchemaResponse
|
||||
|
||||
let defaultAction = throw400 DataConnectorError "Unexpected data connector capabilities response - Unexpected Type"
|
||||
capabilitiesAction API.CapabilitiesResponse {..} = pure $ DC.DataConnectorInfo options _crCapabilities _crConfigSchemaResponse
|
||||
|
||||
capabilitiesCase defaultAction capabilitiesAction errorAction capabilitiesU
|
||||
|
||||
resolveSourceConfig' ::
|
||||
MonadIO m =>
|
||||
@ -144,11 +150,15 @@ resolveSourceConfig'
|
||||
|
||||
validateConfiguration sourceName dataConnectorName _dciConfigSchemaResponse transformedConfig
|
||||
|
||||
schemaResponse <-
|
||||
schemaResponseU <-
|
||||
runTraceTWithReporter noReporter "resolve source"
|
||||
. flip runAgentClientT (AgentClientContext logger _dcoUri manager (DC.sourceTimeoutMicroseconds <$> timeout))
|
||||
$ (genericClient // API._schema) (toTxt sourceName) transformedConfig
|
||||
|
||||
let defaultAction = throw400 DataConnectorError "Unexpected data connector schema response - Unexpected Type"
|
||||
|
||||
schemaResponse <- schemaCase defaultAction pure errorAction schemaResponseU
|
||||
|
||||
pure
|
||||
DC.SourceConfig
|
||||
{ _scEndpoint = _dcoUri,
|
||||
@ -384,3 +394,6 @@ columnTypeToScalarType = \case
|
||||
RQL.T.C.ColumnScalar scalarType -> scalarType
|
||||
-- NOTE: This should be unreachable:
|
||||
RQL.T.C.ColumnEnumReference _ -> DC.StringTy
|
||||
|
||||
errorAction :: MonadError QErr m => API.ErrorResponse -> m a
|
||||
errorAction e = throw400WithDetail DataConnectorError (errorResponseSummary e) (_crDetails e)
|
||||
|
@ -20,6 +20,7 @@ module Hasura.Base.Error
|
||||
internalError,
|
||||
QErrM,
|
||||
throw400,
|
||||
throw400WithDetail,
|
||||
throw404,
|
||||
throw405,
|
||||
throw409,
|
||||
@ -292,6 +293,10 @@ type QErrM m = (MonadError QErr m)
|
||||
throw400 :: (QErrM m) => Code -> Text -> m a
|
||||
throw400 c t = throwError $ err400 c t
|
||||
|
||||
throw400WithDetail :: (QErrM m) => Code -> Text -> Value -> m a
|
||||
throw400WithDetail c t detail =
|
||||
throwError $ (err400 c t) {qeInternal = Just $ ExtraInternal detail}
|
||||
|
||||
throw404 :: (QErrM m) => Text -> m a
|
||||
throw404 t = throwError $ err404 NotFound t
|
||||
|
||||
|
@ -42,7 +42,9 @@ import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap
|
||||
import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||
import Data.Text.Extended
|
||||
import Data.Text.Extended qualified as Text.E
|
||||
import Hasura.Backends.DataConnector.API (errorResponseSummary, schemaCase)
|
||||
import Hasura.Backends.DataConnector.API qualified as API
|
||||
import Hasura.Backends.DataConnector.API.V0.ErrorResponse (_crDetails)
|
||||
import Hasura.Backends.DataConnector.Adapter.Types qualified as DC.Types
|
||||
import Hasura.Backends.DataConnector.Agent.Client qualified as Agent.Client
|
||||
import Hasura.Base.Error
|
||||
@ -74,6 +76,7 @@ import Hasura.SQL.BackendMap qualified as BackendMap
|
||||
import Hasura.Server.Logging (MetadataLog (..))
|
||||
import Hasura.Tracing qualified as Tracing
|
||||
import Network.HTTP.Client.Manager qualified as HTTP.Manager
|
||||
import Servant.API (Union)
|
||||
import Servant.Client ((//))
|
||||
import Servant.Client.Generic qualified as Servant.Client
|
||||
|
||||
@ -364,7 +367,7 @@ runGetSourceTables GetSourceTables {..} = do
|
||||
schemaResponse <-
|
||||
Tracing.runTraceTWithReporter Tracing.noReporter "resolve source"
|
||||
. flip Agent.Client.runAgentClientT (Agent.Client.AgentClientContext logger _dcoUri manager (DC.Types.sourceTimeoutMicroseconds <$> timeout))
|
||||
$ (Servant.Client.genericClient // API._schema) (Text.E.toTxt _gstSourceName) apiConfig
|
||||
$ schemaGuard =<< (Servant.Client.genericClient // API._schema) (Text.E.toTxt _gstSourceName) apiConfig
|
||||
|
||||
let fullyQualifiedTableNames = fmap API._tiName $ API._srTables schemaResponse
|
||||
pure $ EncJSON.encJFromJValue fullyQualifiedTableNames
|
||||
@ -422,8 +425,14 @@ runGetTableInfo GetTableInfo {..} = do
|
||||
schemaResponse <-
|
||||
Tracing.runTraceTWithReporter Tracing.noReporter "resolve source"
|
||||
. flip Agent.Client.runAgentClientT (Agent.Client.AgentClientContext logger _dcoUri manager (DC.Types.sourceTimeoutMicroseconds <$> timeout))
|
||||
$ (Servant.Client.genericClient // API._schema) (Text.E.toTxt _gtiSourceName) apiConfig
|
||||
$ schemaGuard =<< (Servant.Client.genericClient // API._schema) (Text.E.toTxt _gtiSourceName) apiConfig
|
||||
|
||||
let table = find ((== _gtiTableName) . API._tiName) $ API._srTables schemaResponse
|
||||
pure $ EncJSON.encJFromJValue table
|
||||
backend -> Error.throw500 ("Schema fetching is not supported for '" <> Text.E.toTxt backend <> "'")
|
||||
|
||||
schemaGuard :: MonadError QErr m => Union '[API.SchemaResponse, API.ErrorResponse] -> m API.SchemaResponse
|
||||
schemaGuard = schemaCase defaultAction pure errorAction
|
||||
where
|
||||
defaultAction = throw400 DataConnectorError "Error resolving source schema"
|
||||
errorAction e = throw400WithDetail DataConnectorError ("Error resolving source schema: " <> errorResponseSummary e) (_crDetails e)
|
||||
|
Loading…
Reference in New Issue
Block a user