mirror of
https://github.com/hasura/graphql-engine.git
synced 2025-01-07 16:21:52 +03:00
Data Connectors insert mutations support [GDC-609]
[GDC-609]: https://hasurahq.atlassian.net/browse/GDC-609?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7513 GitOrigin-RevId: cb401e3ed84c5b60ec59b63dc478a1162b360135
This commit is contained in:
parent
06b284cf33
commit
bfdeaf0334
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hasura/dc-api-types",
|
"name": "@hasura/dc-api-types",
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"description": "Hasura GraphQL Engine Data Connector Agent API types",
|
"description": "Hasura GraphQL Engine Data Connector Agent API types",
|
||||||
"author": "Hasura (https://github.com/hasura/graphql-engine)",
|
"author": "Hasura (https://github.com/hasura/graphql-engine)",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
@ -2120,6 +2120,9 @@
|
|||||||
},
|
},
|
||||||
"InsertMutationOperation": {
|
"InsertMutationOperation": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"post_insert_check": {
|
||||||
|
"$ref": "#/components/schemas/Expression"
|
||||||
|
},
|
||||||
"returning_fields": {
|
"returning_fields": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Field"
|
"$ref": "#/components/schemas/Field"
|
||||||
@ -2258,6 +2261,9 @@
|
|||||||
},
|
},
|
||||||
"UpdateMutationOperation": {
|
"UpdateMutationOperation": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"post_update_check": {
|
||||||
|
"$ref": "#/components/schemas/Expression"
|
||||||
|
},
|
||||||
"returning_fields": {
|
"returning_fields": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/Field"
|
"$ref": "#/components/schemas/Field"
|
||||||
|
@ -2,11 +2,13 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
|
import type { Expression } from './Expression';
|
||||||
import type { Field } from './Field';
|
import type { Field } from './Field';
|
||||||
import type { RowObject } from './RowObject';
|
import type { RowObject } from './RowObject';
|
||||||
import type { TableName } from './TableName';
|
import type { TableName } from './TableName';
|
||||||
|
|
||||||
export type InsertMutationOperation = {
|
export type InsertMutationOperation = {
|
||||||
|
post_insert_check?: Expression;
|
||||||
/**
|
/**
|
||||||
* The fields to return for the rows affected by this insert operation
|
* The fields to return for the rows affected by this insert operation
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,7 @@ import type { RowUpdate } from './RowUpdate';
|
|||||||
import type { TableName } from './TableName';
|
import type { TableName } from './TableName';
|
||||||
|
|
||||||
export type UpdateMutationOperation = {
|
export type UpdateMutationOperation = {
|
||||||
|
post_update_check?: Expression;
|
||||||
/**
|
/**
|
||||||
* The fields to return for the rows affected by this update operation
|
* The fields to return for the rows affected by this update operation
|
||||||
*/
|
*/
|
||||||
|
10
dc-agents/package-lock.json
generated
10
dc-agents/package-lock.json
generated
@ -24,7 +24,7 @@
|
|||||||
},
|
},
|
||||||
"dc-api-types": {
|
"dc-api-types": {
|
||||||
"name": "@hasura/dc-api-types",
|
"name": "@hasura/dc-api-types",
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
@ -1197,7 +1197,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^7.0.0",
|
"@fastify/cors": "^7.0.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify": "^3.29.0",
|
"fastify": "^3.29.0",
|
||||||
"mathjs": "^11.0.0",
|
"mathjs": "^11.0.0",
|
||||||
"pino-pretty": "^8.0.0",
|
"pino-pretty": "^8.0.0",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.1.0",
|
"@fastify/cors": "^8.1.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify": "^4.4.0",
|
"fastify": "^4.4.0",
|
||||||
"fastify-metrics": "^9.2.1",
|
"fastify-metrics": "^9.2.1",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
@ -3125,7 +3125,7 @@
|
|||||||
"version": "file:reference",
|
"version": "file:reference",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fastify/cors": "^7.0.0",
|
"@fastify/cors": "^7.0.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
"@types/node": "^16.11.49",
|
"@types/node": "^16.11.49",
|
||||||
"@types/xml2js": "^0.4.11",
|
"@types/xml2js": "^0.4.11",
|
||||||
@ -3514,7 +3514,7 @@
|
|||||||
"version": "file:sqlite",
|
"version": "file:sqlite",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@fastify/cors": "^8.1.0",
|
"@fastify/cors": "^8.1.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
"@types/node": "^16.11.49",
|
"@types/node": "^16.11.49",
|
||||||
"@types/sqlite3": "^3.1.8",
|
"@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",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^7.0.0",
|
"@fastify/cors": "^7.0.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify": "^3.29.0",
|
"fastify": "^3.29.0",
|
||||||
"mathjs": "^11.0.0",
|
"mathjs": "^11.0.0",
|
||||||
"pino-pretty": "^8.0.0",
|
"pino-pretty": "^8.0.0",
|
||||||
@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@hasura/dc-api-types": {
|
"node_modules/@hasura/dc-api-types": {
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^7.0.0",
|
"@fastify/cors": "^7.0.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify": "^3.29.0",
|
"fastify": "^3.29.0",
|
||||||
"mathjs": "^11.0.0",
|
"mathjs": "^11.0.0",
|
||||||
"pino-pretty": "^8.0.0",
|
"pino-pretty": "^8.0.0",
|
||||||
|
4
dc-agents/sqlite/package-lock.json
generated
4
dc-agents/sqlite/package-lock.json
generated
@ -10,7 +10,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.1.0",
|
"@fastify/cors": "^8.1.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify": "^4.4.0",
|
"fastify": "^4.4.0",
|
||||||
"fastify-metrics": "^9.2.1",
|
"fastify-metrics": "^9.2.1",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
@ -54,7 +54,7 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@hasura/dc-api-types": {
|
"node_modules/@hasura/dc-api-types": {
|
||||||
"version": "0.20.0",
|
"version": "0.21.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node16": "^1.0.3",
|
"@tsconfig/node16": "^1.0.3",
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cors": "^8.1.0",
|
"@fastify/cors": "^8.1.0",
|
||||||
"@hasura/dc-api-types": "0.20.0",
|
"@hasura/dc-api-types": "0.21.0",
|
||||||
"fastify-metrics": "^9.2.1",
|
"fastify-metrics": "^9.2.1",
|
||||||
"fastify": "^4.4.0",
|
"fastify": "^4.4.0",
|
||||||
"nanoid": "^3.3.4",
|
"nanoid": "^3.3.4",
|
||||||
|
@ -4,13 +4,12 @@ import { getSchema } from './schema';
|
|||||||
import { explain, queryData } from './query';
|
import { explain, queryData } from './query';
|
||||||
import { getConfig, tryGetConfig } from './config';
|
import { getConfig, tryGetConfig } from './config';
|
||||||
import { capabilitiesResponse } from './capabilities';
|
import { capabilitiesResponse } from './capabilities';
|
||||||
import { QueryResponse, SchemaResponse, QueryRequest, CapabilitiesResponse, ExplainResponse, RawRequest, RawResponse, ErrorResponse } from '@hasura/dc-api-types';
|
import { QueryResponse, SchemaResponse, QueryRequest, CapabilitiesResponse, ExplainResponse, RawRequest, RawResponse, ErrorResponse, MutationRequest, MutationResponse } from '@hasura/dc-api-types';
|
||||||
import { connect } from './db';
|
import { connect } from './db';
|
||||||
import metrics from 'fastify-metrics';
|
import metrics from 'fastify-metrics';
|
||||||
import prometheus from 'prom-client';
|
import prometheus from 'prom-client';
|
||||||
import * as fs from 'fs'
|
|
||||||
import { runRawOperation } from './raw';
|
import { runRawOperation } from './raw';
|
||||||
import { LOG_LEVEL, METRICS, PERMISSIVE_CORS, PRETTY_PRINT_LOGS } from './environment';
|
import { LOG_LEVEL, METRICS, MUTATIONS, PERMISSIVE_CORS, PRETTY_PRINT_LOGS } from './environment';
|
||||||
|
|
||||||
const port = Number(process.env.PORT) || 8100;
|
const port = Number(process.env.PORT) || 8100;
|
||||||
|
|
||||||
@ -144,6 +143,13 @@ server.post<{ Body: QueryRequest, Reply: ExplainResponse}>("/explain", async (re
|
|||||||
return explain(config, sqlLogger, request.body);
|
return explain(config, sqlLogger, request.body);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (MUTATIONS) {
|
||||||
|
server.post<{ Body: MutationRequest, Reply: MutationResponse}>("/mutation", async (request, _response) => {
|
||||||
|
server.log.info({ headers: request.headers, query: request.body, }, "mutation.request");
|
||||||
|
throw Error("Mutations not yet implemented");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
server.get("/health", async (request, response) => {
|
server.get("/health", async (request, response) => {
|
||||||
const config = tryGetConfig(request);
|
const config = tryGetConfig(request);
|
||||||
response.type('application/json');
|
response.type('application/json');
|
||||||
@ -164,13 +170,6 @@ server.get("/health", async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get("/swagger.json", async (request, response) => {
|
|
||||||
fs.readFile('src/types/agent.openapi.json', (err, fileBuffer) => {
|
|
||||||
response.type('application/json');
|
|
||||||
response.send(err || fileBuffer)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
server.get("/", async (request, response) => {
|
server.get("/", async (request, response) => {
|
||||||
response.type('text/html');
|
response.type('text/html');
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
@ -187,9 +186,9 @@ server.get("/", async (request, response) => {
|
|||||||
<li><a href="/capabilities">GET /capabilities - Capabilities Metadata</a>
|
<li><a href="/capabilities">GET /capabilities - Capabilities Metadata</a>
|
||||||
<li><a href="/schema">GET /schema - Agent Schema</a>
|
<li><a href="/schema">GET /schema - Agent Schema</a>
|
||||||
<li><a href="/query">POST /query - Query Handler</a>
|
<li><a href="/query">POST /query - Query Handler</a>
|
||||||
|
<li><a href="/mutation">POST /mutation - Mutation Handler</a>
|
||||||
<li><a href="/raw">POST /raw - Raw Query Handler</a>
|
<li><a href="/raw">POST /raw - Raw Query Handler</a>
|
||||||
<li><a href="/health">GET /health - Healthcheck</a>
|
<li><a href="/health">GET /health - Healthcheck</a>
|
||||||
<li><a href="/swagger.json">GET /swagger.json - Swagger JSON</a>
|
|
||||||
<li><a href="/metrics">GET /metrics - Prometheus formatted metrics</a>
|
<li><a href="/metrics">GET /metrics - Prometheus formatted metrics</a>
|
||||||
</ul>
|
</ul>
|
||||||
</body>
|
</body>
|
||||||
|
@ -654,7 +654,9 @@ library
|
|||||||
, Hasura.Backends.DataConnector.Adapter.Types.Mutations
|
, Hasura.Backends.DataConnector.Adapter.Types.Mutations
|
||||||
, Hasura.Backends.DataConnector.Agent.Client
|
, Hasura.Backends.DataConnector.Agent.Client
|
||||||
, Hasura.Backends.DataConnector.Logging
|
, Hasura.Backends.DataConnector.Logging
|
||||||
, Hasura.Backends.DataConnector.Plan
|
, Hasura.Backends.DataConnector.Plan.Common
|
||||||
|
, Hasura.Backends.DataConnector.Plan.MutationPlan
|
||||||
|
, Hasura.Backends.DataConnector.Plan.QueryPlan
|
||||||
|
|
||||||
-- Exposed for benchmark:
|
-- Exposed for benchmark:
|
||||||
, Hasura.Cache.Bounded
|
, Hasura.Cache.Bounded
|
||||||
|
@ -100,6 +100,7 @@ library
|
|||||||
Test.DataConnector.MockAgent.BasicQuerySpec
|
Test.DataConnector.MockAgent.BasicQuerySpec
|
||||||
Test.DataConnector.MockAgent.CustomScalarsSpec
|
Test.DataConnector.MockAgent.CustomScalarsSpec
|
||||||
Test.DataConnector.MockAgent.ErrorSpec
|
Test.DataConnector.MockAgent.ErrorSpec
|
||||||
|
Test.DataConnector.MockAgent.InsertMutationsSpec
|
||||||
Test.DataConnector.MockAgent.MetadataApiSpec
|
Test.DataConnector.MockAgent.MetadataApiSpec
|
||||||
Test.DataConnector.MockAgent.QueryRelationshipsSpec
|
Test.DataConnector.MockAgent.QueryRelationshipsSpec
|
||||||
Test.DataConnector.MockAgent.TransformedConfigurationSpec
|
Test.DataConnector.MockAgent.TransformedConfigurationSpec
|
||||||
|
@ -0,0 +1,250 @@
|
|||||||
|
module Test.DataConnector.MockAgent.InsertMutationsSpec
|
||||||
|
( spec,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Data.Aeson qualified as Aeson
|
||||||
|
import Data.ByteString (ByteString)
|
||||||
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
import Data.List.NonEmpty qualified as NE
|
||||||
|
import Harness.Backend.DataConnector.Mock (AgentRequest (..), MockRequestResults (..), mockAgentTest, mockMutationResponse)
|
||||||
|
import Harness.Backend.DataConnector.Mock qualified as Mock
|
||||||
|
import Harness.Quoter.Graphql (graphql)
|
||||||
|
import Harness.Quoter.Yaml (yaml)
|
||||||
|
import Harness.Test.BackendType qualified as BackendType
|
||||||
|
import Harness.Test.Fixture qualified as Fixture
|
||||||
|
import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment)
|
||||||
|
import Harness.Yaml
|
||||||
|
import Hasura.Backends.DataConnector.API qualified as API
|
||||||
|
import Hasura.Prelude
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
spec :: SpecWith GlobalTestEnvironment
|
||||||
|
spec =
|
||||||
|
Fixture.runWithLocalTestEnvironment
|
||||||
|
( NE.fromList
|
||||||
|
[ (Fixture.fixture $ Fixture.Backend Mock.backendTypeMetadata)
|
||||||
|
{ Fixture.mkLocalTestEnvironment = Mock.mkLocalTestEnvironment,
|
||||||
|
Fixture.setupTeardown = \(testEnv, mockEnv) ->
|
||||||
|
[Mock.setupAction sourceMetadata Mock.agentConfig (testEnv, mockEnv)]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
tests
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
testRoleName :: ByteString
|
||||||
|
testRoleName = "test-role"
|
||||||
|
|
||||||
|
sourceMetadata :: Aeson.Value
|
||||||
|
sourceMetadata =
|
||||||
|
let source = BackendType.backendSourceName Mock.backendTypeMetadata
|
||||||
|
backendType = BackendType.backendTypeString Mock.backendTypeMetadata
|
||||||
|
in [yaml|
|
||||||
|
name : *source
|
||||||
|
kind: *backendType
|
||||||
|
tables:
|
||||||
|
- table: [Album]
|
||||||
|
object_relationships:
|
||||||
|
- name: Artist
|
||||||
|
using:
|
||||||
|
manual_configuration:
|
||||||
|
remote_table: [Artist]
|
||||||
|
column_mapping:
|
||||||
|
ArtistId: ArtistId
|
||||||
|
select_permissions:
|
||||||
|
- role: *testRoleName
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- AlbumId
|
||||||
|
- ArtistId
|
||||||
|
- Title
|
||||||
|
filter: {}
|
||||||
|
insert_permissions:
|
||||||
|
- role: *testRoleName
|
||||||
|
permission:
|
||||||
|
check:
|
||||||
|
ArtistId: "X-Hasura-ArtistId"
|
||||||
|
set:
|
||||||
|
ArtistId: "X-Hasura-ArtistId"
|
||||||
|
columns:
|
||||||
|
- AlbumId
|
||||||
|
- ArtistId
|
||||||
|
- Title
|
||||||
|
- table: [Artist]
|
||||||
|
select_permissions:
|
||||||
|
- role: *testRoleName
|
||||||
|
permission:
|
||||||
|
columns:
|
||||||
|
- ArtistId
|
||||||
|
- Name
|
||||||
|
filter: {}
|
||||||
|
configuration: {}
|
||||||
|
|]
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
tests :: Fixture.Options -> SpecWith (TestEnvironment, Mock.MockAgentEnvironment)
|
||||||
|
tests _opts = do
|
||||||
|
mockAgentTest "insert multiple rows with insert permissions" $ \performGraphqlRequest -> do
|
||||||
|
let headers = [("X-Hasura-ArtistId", "2"), ("X-Hasura-Role", testRoleName)]
|
||||||
|
let graphqlRequest =
|
||||||
|
[graphql|
|
||||||
|
mutation InsertMutation {
|
||||||
|
insert_Album(objects: [
|
||||||
|
{AlbumId: 9001, Title: "Super Mega Rock"},
|
||||||
|
{AlbumId: 9002, Title: "Accept This"}
|
||||||
|
]) {
|
||||||
|
insertCount: affected_rows
|
||||||
|
insertedRows: returning {
|
||||||
|
AlbumId
|
||||||
|
Title
|
||||||
|
Artist {
|
||||||
|
ArtistId
|
||||||
|
Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
let mockAgentResponse =
|
||||||
|
API.MutationResponse
|
||||||
|
[ API.MutationOperationResults
|
||||||
|
{ API._morAffectedRows = 2,
|
||||||
|
API._morReturning =
|
||||||
|
Just
|
||||||
|
[ HashMap.fromList
|
||||||
|
[ (API.FieldName "insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9001),
|
||||||
|
(API.FieldName "insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Super Mega Rock"),
|
||||||
|
( API.FieldName "insertedRows_Artist",
|
||||||
|
API.mkRelationshipFieldValue $
|
||||||
|
rowsResponse
|
||||||
|
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 2),
|
||||||
|
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Accept")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
],
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "insertedRows_AlbumId", API.mkColumnFieldValue $ Aeson.Number 9002),
|
||||||
|
(API.FieldName "insertedRows_Title", API.mkColumnFieldValue $ Aeson.String "Accept This"),
|
||||||
|
( API.FieldName "insertedRows_Artist",
|
||||||
|
API.mkRelationshipFieldValue $
|
||||||
|
rowsResponse
|
||||||
|
[ [ (API.FieldName "ArtistId", API.mkColumnFieldValue $ Aeson.Number 2),
|
||||||
|
(API.FieldName "Name", API.mkColumnFieldValue $ Aeson.String "Accept")
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
let mockConfig = Mock.chinookMock & mockMutationResponse mockAgentResponse
|
||||||
|
|
||||||
|
MockRequestResults {..} <- performGraphqlRequest mockConfig headers graphqlRequest
|
||||||
|
|
||||||
|
_mrrGraphqlResponse
|
||||||
|
`shouldBeYaml` [yaml|
|
||||||
|
data:
|
||||||
|
insert_Album:
|
||||||
|
insertCount: 2
|
||||||
|
insertedRows:
|
||||||
|
- AlbumId: 9001
|
||||||
|
Title: Super Mega Rock
|
||||||
|
Artist:
|
||||||
|
ArtistId: 2
|
||||||
|
Name: Accept
|
||||||
|
- AlbumId: 9002
|
||||||
|
Title: Accept This
|
||||||
|
Artist:
|
||||||
|
ArtistId: 2
|
||||||
|
Name: Accept
|
||||||
|
|]
|
||||||
|
|
||||||
|
let expectedRequest =
|
||||||
|
API.MutationRequest
|
||||||
|
{ API._mrTableRelationships =
|
||||||
|
[ API.TableRelationships
|
||||||
|
{ API._trSourceTable = API.TableName ("Album" :| []),
|
||||||
|
API._trRelationships =
|
||||||
|
HashMap.fromList
|
||||||
|
[ ( API.RelationshipName "Artist",
|
||||||
|
API.Relationship
|
||||||
|
{ API._rTargetTable = API.TableName ("Artist" :| []),
|
||||||
|
API._rRelationshipType = API.ObjectRelationship,
|
||||||
|
API._rColumnMapping = HashMap.fromList [(API.ColumnName "ArtistId", API.ColumnName "ArtistId")]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
API._mrInsertSchema =
|
||||||
|
[ API.TableInsertSchema
|
||||||
|
{ API._tisTable = API.TableName ("Album" :| []),
|
||||||
|
API._tisFields =
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "AlbumId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "AlbumId") (API.ScalarType "number")),
|
||||||
|
(API.FieldName "ArtistId", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "ArtistId") (API.ScalarType "number")),
|
||||||
|
(API.FieldName "Title", API.ColumnInsert $ API.ColumnInsertSchema (API.ColumnName "Title") (API.ScalarType "string"))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
API._mrOperations =
|
||||||
|
[ API.InsertOperation $
|
||||||
|
API.InsertMutationOperation
|
||||||
|
{ API._imoTable = API.TableName ("Album" :| []),
|
||||||
|
API._imoRows =
|
||||||
|
[ API.RowObject $
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9001),
|
||||||
|
(API.FieldName "ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2),
|
||||||
|
(API.FieldName "Title", API.mkColumnInsertFieldValue $ Aeson.String "Super Mega Rock")
|
||||||
|
],
|
||||||
|
API.RowObject $
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "AlbumId", API.mkColumnInsertFieldValue $ Aeson.Number 9002),
|
||||||
|
(API.FieldName "ArtistId", API.mkColumnInsertFieldValue $ Aeson.Number 2),
|
||||||
|
(API.FieldName "Title", API.mkColumnInsertFieldValue $ Aeson.String "Accept This")
|
||||||
|
]
|
||||||
|
],
|
||||||
|
API._imoPostInsertCheck =
|
||||||
|
Just $
|
||||||
|
API.ApplyBinaryComparisonOperator
|
||||||
|
API.Equal
|
||||||
|
(API.ComparisonColumn API.CurrentTable (API.ColumnName "ArtistId") $ API.ScalarType "number")
|
||||||
|
(API.ScalarValue (Aeson.Number 2) $ API.ScalarType "number"),
|
||||||
|
API._imoReturningFields =
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "insertedRows_AlbumId", API.ColumnField (API.ColumnName "AlbumId") (API.ScalarType "number")),
|
||||||
|
(API.FieldName "insertedRows_Title", API.ColumnField (API.ColumnName "Title") (API.ScalarType "string")),
|
||||||
|
( API.FieldName "insertedRows_Artist",
|
||||||
|
API.RelField
|
||||||
|
( API.RelationshipField
|
||||||
|
(API.RelationshipName "Artist")
|
||||||
|
API.Query
|
||||||
|
{ _qFields =
|
||||||
|
Just $
|
||||||
|
HashMap.fromList
|
||||||
|
[ (API.FieldName "ArtistId", API.ColumnField (API.ColumnName "ArtistId") $ API.ScalarType "number"),
|
||||||
|
(API.FieldName "Name", API.ColumnField (API.ColumnName "Name") $ API.ScalarType "string")
|
||||||
|
],
|
||||||
|
_qAggregates = Nothing,
|
||||||
|
_qLimit = Nothing,
|
||||||
|
_qOffset = Nothing,
|
||||||
|
_qWhere = Nothing,
|
||||||
|
_qOrderBy = Nothing
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
_mrrRecordedRequest `shouldBe` Just (Mutation expectedRequest)
|
||||||
|
|
||||||
|
rowsResponse :: [[(API.FieldName, API.FieldValue)]] -> API.QueryResponse
|
||||||
|
rowsResponse rows = API.QueryResponse (Just $ HashMap.fromList <$> rows) Nothing
|
@ -59,7 +59,7 @@ sourceMetadata =
|
|||||||
tests :: Fixture.Options -> SpecWith (TestEnvironment, Mock.MockAgentEnvironment)
|
tests :: Fixture.Options -> SpecWith (TestEnvironment, Mock.MockAgentEnvironment)
|
||||||
tests opts = do
|
tests opts = do
|
||||||
describe "MetadataAPI Mock Tests" $ do
|
describe "MetadataAPI Mock Tests" $ do
|
||||||
it "Should peform a template transform when calling _get_source_tables" $ \(testEnvironment, Mock.MockAgentEnvironment {maeQueryConfig}) -> do
|
it "Should peform a template transform when calling _get_source_tables" $ \(testEnvironment, Mock.MockAgentEnvironment {maeRecordedRequestConfig}) -> do
|
||||||
let sortYamlArray :: Aeson.Value -> IO Aeson.Value
|
let sortYamlArray :: Aeson.Value -> IO Aeson.Value
|
||||||
sortYamlArray (Aeson.Array a) = pure $ Aeson.Array (Vector.fromList (sort (Vector.toList a)))
|
sortYamlArray (Aeson.Array a) = pure $ Aeson.Array (Vector.fromList (sort (Vector.toList a)))
|
||||||
sortYamlArray _ = fail "Should return Array"
|
sortYamlArray _ = fail "Should return Array"
|
||||||
@ -67,8 +67,8 @@ tests opts = do
|
|||||||
case BackendType.backendSourceName <$> getBackendTypeConfig testEnvironment of
|
case BackendType.backendSourceName <$> getBackendTypeConfig testEnvironment of
|
||||||
Nothing -> pendingWith "Backend not found for testEnvironment"
|
Nothing -> pendingWith "Backend not found for testEnvironment"
|
||||||
Just sourceString -> do
|
Just sourceString -> do
|
||||||
queryConfig <- IORef.readIORef maeQueryConfig
|
queryConfig <- IORef.readIORef maeRecordedRequestConfig
|
||||||
IORef.writeIORef maeQueryConfig Nothing
|
IORef.writeIORef maeRecordedRequestConfig Nothing
|
||||||
|
|
||||||
queryConfig `shouldBe` Just (Config $ KM.fromList [("DEBUG", Aeson.Object (KM.fromList [("test", Aeson.String "data")]))])
|
queryConfig `shouldBe` Just (Config $ KM.fromList [("DEBUG", Aeson.Object (KM.fromList [("test", Aeson.String "data")]))])
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ module Hasura.Backends.DataConnector.API
|
|||||||
capabilitiesCase,
|
capabilitiesCase,
|
||||||
schemaCase,
|
schemaCase,
|
||||||
queryCase,
|
queryCase,
|
||||||
|
mutationCase,
|
||||||
openApiSchema,
|
openApiSchema,
|
||||||
Routes (..),
|
Routes (..),
|
||||||
apiClient,
|
apiClient,
|
||||||
@ -99,6 +100,19 @@ queryCase defaultAction queryAction errorAction union = do
|
|||||||
|
|
||||||
type QueryResponses = '[V0.QueryResponse, V0.ErrorResponse, V0.ErrorResponse400]
|
type QueryResponses = '[V0.QueryResponse, V0.ErrorResponse, V0.ErrorResponse400]
|
||||||
|
|
||||||
|
-- | This function defines a central place to ensure that all cases are covered for mutation and error responses.
|
||||||
|
-- When additional responses are added to the Union, this should be updated to ensure that all responses have been considered.
|
||||||
|
mutationCase :: a -> (MutationResponse -> a) -> (ErrorResponse -> a) -> Union MutationResponses -> a
|
||||||
|
mutationCase defaultAction mutationAction errorAction union = do
|
||||||
|
let mutationM = matchUnion @MutationResponse union
|
||||||
|
let errorM = matchUnion @ErrorResponse union
|
||||||
|
let errorM400 = matchUnion @ErrorResponse400 union
|
||||||
|
case (mutationM, errorM, errorM400) of
|
||||||
|
(Nothing, Nothing, Nothing) -> defaultAction
|
||||||
|
(Just c, _, _) -> mutationAction c
|
||||||
|
(_, Just e, _) -> errorAction e
|
||||||
|
(_, _, Just (WithStatus e)) -> errorAction e
|
||||||
|
|
||||||
type MutationResponses = '[V0.MutationResponse, V0.ErrorResponse, V0.ErrorResponse400]
|
type MutationResponses = '[V0.MutationResponse, V0.ErrorResponse, V0.ErrorResponse400]
|
||||||
|
|
||||||
type QueryApi =
|
type QueryApi =
|
||||||
|
@ -214,6 +214,8 @@ instance HasCodec MutationOperation where
|
|||||||
.= _imoTable
|
.= _imoTable
|
||||||
<*> requiredField "rows" "The rows to insert into the table"
|
<*> requiredField "rows" "The rows to insert into the table"
|
||||||
.= _imoRows
|
.= _imoRows
|
||||||
|
<*> optionalFieldOrNull "post_insert_check" "An expression that all inserted rows must match after they have been inserted, otherwise the changes must be reverted"
|
||||||
|
.= _imoPostInsertCheck
|
||||||
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this insert operation"
|
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this insert operation"
|
||||||
.= _imoReturningFields
|
.= _imoReturningFields
|
||||||
|
|
||||||
@ -226,6 +228,8 @@ instance HasCodec MutationOperation where
|
|||||||
.= _umoWhere
|
.= _umoWhere
|
||||||
<*> requiredField "updates" "The updates to make to the matched rows in the table"
|
<*> requiredField "updates" "The updates to make to the matched rows in the table"
|
||||||
.= _umoUpdates
|
.= _umoUpdates
|
||||||
|
<*> optionalFieldOrNull "post_update_check" "An expression that all updated rows must match after they have been updated, otherwise the changes must be reverted"
|
||||||
|
.= _umoPostUpdateCheck
|
||||||
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this update operation"
|
<*> optionalFieldOrNullWithOmittedDefault "returning_fields" mempty "The fields to return for the rows affected by this update operation"
|
||||||
.= _umoReturningFields
|
.= _umoReturningFields
|
||||||
|
|
||||||
@ -258,6 +262,9 @@ data InsertMutationOperation = InsertMutationOperation
|
|||||||
_imoTable :: API.V0.TableName,
|
_imoTable :: API.V0.TableName,
|
||||||
-- | The rows to insert into the table
|
-- | The rows to insert into the table
|
||||||
_imoRows :: [RowObject],
|
_imoRows :: [RowObject],
|
||||||
|
-- | An expression that all inserted rows must match after they have been inserted,
|
||||||
|
-- otherwise the changes must be reverted
|
||||||
|
_imoPostInsertCheck :: Maybe API.V0.Expression,
|
||||||
-- | The fields to return that represent a projection over the set of rows inserted
|
-- | The fields to return that represent a projection over the set of rows inserted
|
||||||
-- after they are inserted (after insertion they include calculated columns, relations etc)
|
-- after they are inserted (after insertion they include calculated columns, relations etc)
|
||||||
_imoReturningFields :: HashMap API.V0.FieldName API.V0.Field
|
_imoReturningFields :: HashMap API.V0.FieldName API.V0.Field
|
||||||
@ -370,6 +377,9 @@ data UpdateMutationOperation = UpdateMutationOperation
|
|||||||
_umoWhere :: Maybe API.V0.Expression,
|
_umoWhere :: Maybe API.V0.Expression,
|
||||||
-- | The updates to perform against each row
|
-- | The updates to perform against each row
|
||||||
_umoUpdates :: [RowUpdate],
|
_umoUpdates :: [RowUpdate],
|
||||||
|
-- | An expression that all updated rows must match after they have been updated,
|
||||||
|
-- otherwise the changes must be reverted
|
||||||
|
_umoPostUpdateCheck :: Maybe API.V0.Expression,
|
||||||
-- | The fields to return that represent a projection over the set of rows updated
|
-- | The fields to return that represent a projection over the set of rows updated
|
||||||
-- after they are updated (ie. with their updated values)
|
-- after they are updated (ie. with their updated values)
|
||||||
_umoReturningFields :: HashMap API.V0.FieldName API.V0.Field
|
_umoReturningFields :: HashMap API.V0.FieldName API.V0.Field
|
||||||
|
@ -19,6 +19,11 @@ module Harness.Backend.DataConnector.Mock
|
|||||||
mockAgentPort,
|
mockAgentPort,
|
||||||
defaultTestCase,
|
defaultTestCase,
|
||||||
chinookMock,
|
chinookMock,
|
||||||
|
AgentRequest (..),
|
||||||
|
MockRequestResults (..),
|
||||||
|
mockQueryResponse,
|
||||||
|
mockMutationResponse,
|
||||||
|
mockAgentTest,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -40,14 +45,14 @@ import Harness.TestEnvironment (TestEnvironment (..))
|
|||||||
import Harness.Yaml (shouldReturnYaml)
|
import Harness.Yaml (shouldReturnYaml)
|
||||||
import Hasura.Backends.DataConnector.API qualified as API
|
import Hasura.Backends.DataConnector.API qualified as API
|
||||||
import Hasura.Prelude
|
import Hasura.Prelude
|
||||||
import Test.Hspec (shouldBe)
|
import Test.Hspec (Arg, Expectation, SpecWith, it, shouldBe)
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
backendTypeMetadata :: BackendType.BackendTypeConfig
|
backendTypeMetadata :: BackendType.BackendTypeConfig
|
||||||
backendTypeMetadata =
|
backendTypeMetadata =
|
||||||
BackendType.BackendTypeConfig
|
BackendType.BackendTypeConfig
|
||||||
{ backendType = BackendType.DataConnectorReference,
|
{ backendType = BackendType.DataConnectorMock,
|
||||||
backendSourceName = "mock",
|
backendSourceName = "mock",
|
||||||
backendCapabilities = Nothing,
|
backendCapabilities = Nothing,
|
||||||
backendTypeString = "mock",
|
backendTypeString = "mock",
|
||||||
@ -125,18 +130,18 @@ dataconnector:
|
|||||||
|
|
||||||
data MockAgentEnvironment = MockAgentEnvironment
|
data MockAgentEnvironment = MockAgentEnvironment
|
||||||
{ maeConfig :: I.IORef MockConfig,
|
{ maeConfig :: I.IORef MockConfig,
|
||||||
maeQuery :: I.IORef (Maybe API.QueryRequest),
|
maeRecordedRequest :: I.IORef (Maybe AgentRequest),
|
||||||
maeThread :: Async (),
|
maeThread :: Async (),
|
||||||
maeQueryConfig :: I.IORef (Maybe API.Config)
|
maeRecordedRequestConfig :: I.IORef (Maybe API.Config)
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Create the 'I.IORef's and launch the servant mock agent.
|
-- | Create the 'I.IORef's and launch the servant mock agent.
|
||||||
mkLocalTestEnvironment :: TestEnvironment -> Managed MockAgentEnvironment
|
mkLocalTestEnvironment :: TestEnvironment -> Managed MockAgentEnvironment
|
||||||
mkLocalTestEnvironment _ = mkTestResource do
|
mkLocalTestEnvironment _ = mkTestResource do
|
||||||
maeConfig <- I.newIORef chinookMock
|
maeConfig <- I.newIORef chinookMock
|
||||||
maeQuery <- I.newIORef Nothing
|
maeRecordedRequest <- I.newIORef Nothing
|
||||||
maeQueryConfig <- I.newIORef Nothing
|
maeRecordedRequestConfig <- I.newIORef Nothing
|
||||||
maeThread <- Async.async $ runMockServer maeConfig maeQuery maeQueryConfig
|
maeThread <- Async.async $ runMockServer maeConfig maeRecordedRequest maeRecordedRequestConfig
|
||||||
pure $
|
pure $
|
||||||
AcquiredResource
|
AcquiredResource
|
||||||
{ resourceValue = MockAgentEnvironment {..},
|
{ resourceValue = MockAgentEnvironment {..},
|
||||||
@ -201,13 +206,49 @@ runTest opts TestCase {..} (testEnvironment, MockAgentEnvironment {..}) = do
|
|||||||
_then
|
_then
|
||||||
|
|
||||||
-- Read the logged 'API.QueryRequest' from the Agent
|
-- Read the logged 'API.QueryRequest' from the Agent
|
||||||
query <- I.readIORef maeQuery
|
query <- (>>= \case Query query -> Just query; _ -> Nothing) <$> I.readIORef maeRecordedRequest
|
||||||
I.writeIORef maeQuery Nothing
|
I.writeIORef maeRecordedRequest Nothing
|
||||||
|
|
||||||
-- Read the logged 'API.Config' from the Agent
|
-- Read the logged 'API.Config' from the Agent
|
||||||
queryConfig <- I.readIORef maeQueryConfig
|
queryConfig <- I.readIORef maeRecordedRequestConfig
|
||||||
I.writeIORef maeQueryConfig Nothing
|
I.writeIORef maeRecordedRequestConfig Nothing
|
||||||
|
|
||||||
-- Assert that the 'API.QueryRequest' was constructed how we expected.
|
-- Assert that the 'API.QueryRequest' was constructed how we expected.
|
||||||
for_ _whenQuery ((query `shouldBe`) . Just)
|
for_ _whenQuery ((query `shouldBe`) . Just)
|
||||||
for_ _whenConfig ((queryConfig `shouldBe`) . Just)
|
for_ _whenConfig ((queryConfig `shouldBe`) . Just)
|
||||||
|
|
||||||
|
data MockRequestResults = MockRequestResults
|
||||||
|
{ _mrrGraphqlResponse :: Aeson.Value,
|
||||||
|
_mrrRecordedRequest :: Maybe AgentRequest,
|
||||||
|
_mrrRecordedRequestConfig :: Maybe API.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
mockQueryResponse :: API.QueryResponse -> MockConfig -> MockConfig
|
||||||
|
mockQueryResponse queryResponse mockConfig =
|
||||||
|
mockConfig {_queryResponse = \_ -> Right queryResponse}
|
||||||
|
|
||||||
|
mockMutationResponse :: API.MutationResponse -> MockConfig -> MockConfig
|
||||||
|
mockMutationResponse mutationResponse mockConfig =
|
||||||
|
mockConfig {_mutationResponse = \_ -> Right mutationResponse}
|
||||||
|
|
||||||
|
mockAgentTest :: String -> ((MockConfig -> RequestHeaders -> Aeson.Value -> IO MockRequestResults) -> Expectation) -> SpecWith (Arg ((TestEnvironment, MockAgentEnvironment) -> Expectation))
|
||||||
|
mockAgentTest name testBody =
|
||||||
|
it name $ \env -> testBody (postMockAgentGraphqlWithHeaders env)
|
||||||
|
|
||||||
|
postMockAgentGraphqlWithHeaders :: (TestEnvironment, MockAgentEnvironment) -> MockConfig -> RequestHeaders -> Aeson.Value -> IO MockRequestResults
|
||||||
|
postMockAgentGraphqlWithHeaders (testEnvironment, MockAgentEnvironment {..}) mockConfig requestHeaders graphqlRequest = do
|
||||||
|
-- Set the Agent with the 'MockConfig'
|
||||||
|
I.writeIORef maeConfig mockConfig
|
||||||
|
|
||||||
|
-- Reset recording state
|
||||||
|
I.writeIORef maeRecordedRequest Nothing
|
||||||
|
I.writeIORef maeRecordedRequestConfig Nothing
|
||||||
|
|
||||||
|
-- Perform GraphQL request
|
||||||
|
graphqlResponse <- GraphqlEngine.postGraphqlWithHeaders testEnvironment requestHeaders graphqlRequest
|
||||||
|
|
||||||
|
-- Capture recordings
|
||||||
|
recordedRequest <- I.readIORef maeRecordedRequest
|
||||||
|
recordedRequestConfig <- I.readIORef maeRecordedRequestConfig
|
||||||
|
|
||||||
|
pure $ MockRequestResults graphqlResponse recordedRequest recordedRequestConfig
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
-- | Mock Agent Warp server backend
|
-- | Mock Agent Warp server backend
|
||||||
module Harness.Backend.DataConnector.Mock.Server
|
module Harness.Backend.DataConnector.Mock.Server
|
||||||
( MockConfig (..),
|
( AgentRequest (..),
|
||||||
|
MockConfig (..),
|
||||||
chinookMock,
|
chinookMock,
|
||||||
mockAgentPort,
|
mockAgentPort,
|
||||||
runMockServer,
|
runMockServer,
|
||||||
@ -23,13 +24,17 @@ import Servant
|
|||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- Note: Only the _queryResponse field allows mock errors at present.
|
data AgentRequest
|
||||||
-- This can be extended at a later point if required.
|
= Schema
|
||||||
--
|
| Query API.QueryRequest
|
||||||
|
| Mutation API.MutationRequest
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
data MockConfig = MockConfig
|
data MockConfig = MockConfig
|
||||||
{ _capabilitiesResponse :: API.CapabilitiesResponse,
|
{ _capabilitiesResponse :: API.CapabilitiesResponse,
|
||||||
_schemaResponse :: API.SchemaResponse,
|
_schemaResponse :: API.SchemaResponse,
|
||||||
_queryResponse :: API.QueryRequest -> Either API.ErrorResponse API.QueryResponse
|
_queryResponse :: API.QueryRequest -> Either API.ErrorResponse API.QueryResponse,
|
||||||
|
_mutationResponse :: API.MutationRequest -> Either API.ErrorResponse API.MutationResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
mkTableName :: Text -> API.TableName
|
mkTableName :: Text -> API.TableName
|
||||||
@ -43,7 +48,15 @@ capabilities =
|
|||||||
API.Capabilities
|
API.Capabilities
|
||||||
{ API._cDataSchema = API.defaultDataSchemaCapabilities,
|
{ API._cDataSchema = API.defaultDataSchemaCapabilities,
|
||||||
API._cQueries = Just API.QueryCapabilities,
|
API._cQueries = Just API.QueryCapabilities,
|
||||||
API._cMutations = Nothing,
|
API._cMutations =
|
||||||
|
Just $
|
||||||
|
API.MutationCapabilities
|
||||||
|
{ API._mcInsertCapabilities = Just API.InsertCapabilities {API._icSupportsNestedInserts = False},
|
||||||
|
API._mcUpdateCapabilities = Just API.UpdateCapabilities,
|
||||||
|
API._mcDeleteCapabilities = Just API.DeleteCapabilities,
|
||||||
|
API._mcAtomicitySupportLevel = Just API.HeterogeneousOperationsAtomicity,
|
||||||
|
API._mcReturningCapabilities = Just API.ReturningCapabilities
|
||||||
|
},
|
||||||
API._cSubscriptions = Nothing,
|
API._cSubscriptions = Nothing,
|
||||||
API._cScalarTypes = scalarTypesCapabilities,
|
API._cScalarTypes = scalarTypesCapabilities,
|
||||||
API._cRelationships = Just API.RelationshipCapabilities {},
|
API._cRelationships = Just API.RelationshipCapabilities {},
|
||||||
@ -744,7 +757,8 @@ chinookMock =
|
|||||||
MockConfig
|
MockConfig
|
||||||
{ _capabilitiesResponse = capabilities,
|
{ _capabilitiesResponse = capabilities,
|
||||||
_schemaResponse = schema,
|
_schemaResponse = schema,
|
||||||
_queryResponse = \_ -> Right $ API.QueryResponse (Just []) Nothing
|
_queryResponse = \_request -> Right $ API.QueryResponse (Just []) Nothing,
|
||||||
|
_mutationResponse = \_request -> Right $ API.MutationResponse []
|
||||||
}
|
}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
@ -754,29 +768,35 @@ mockCapabilitiesHandler mcfg = liftIO $ do
|
|||||||
cfg <- I.readIORef mcfg
|
cfg <- I.readIORef mcfg
|
||||||
pure $ inject $ SOP.I $ _capabilitiesResponse cfg
|
pure $ inject $ SOP.I $ _capabilitiesResponse cfg
|
||||||
|
|
||||||
mockSchemaHandler :: I.IORef MockConfig -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> Handler (Union API.SchemaResponses)
|
mockSchemaHandler :: I.IORef MockConfig -> I.IORef (Maybe AgentRequest) -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> Handler (Union API.SchemaResponses)
|
||||||
mockSchemaHandler mcfg mQueryConfig _sourceName queryConfig = liftIO $ do
|
mockSchemaHandler mcfg mRecordedRequest mRecordedRequestConfig _sourceName requestConfig = liftIO $ do
|
||||||
cfg <- I.readIORef mcfg
|
cfg <- I.readIORef mcfg
|
||||||
I.writeIORef mQueryConfig (Just queryConfig)
|
I.writeIORef mRecordedRequest (Just Schema)
|
||||||
|
I.writeIORef mRecordedRequestConfig (Just requestConfig)
|
||||||
pure $ inject $ SOP.I $ _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 (Union API.QueryResponses)
|
mockQueryHandler :: I.IORef MockConfig -> I.IORef (Maybe AgentRequest) -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> API.QueryRequest -> Handler (Union API.QueryResponses)
|
||||||
mockQueryHandler mcfg mquery mQueryCfg _sourceName queryConfig query = liftIO $ do
|
mockQueryHandler mcfg mRecordedRequest mRecordedRequestConfig _sourceName requestConfig query = liftIO $ do
|
||||||
handler <- fmap _queryResponse $ I.readIORef mcfg
|
handler <- fmap _queryResponse $ I.readIORef mcfg
|
||||||
I.writeIORef mquery (Just query)
|
I.writeIORef mRecordedRequest (Just $ Query query)
|
||||||
I.writeIORef mQueryCfg (Just queryConfig)
|
I.writeIORef mRecordedRequestConfig (Just requestConfig)
|
||||||
case handler query of
|
case handler query of
|
||||||
Left e -> pure $ inject $ SOP.I e
|
Left err -> pure $ inject $ SOP.I err
|
||||||
Right r -> pure $ inject $ SOP.I r
|
Right response -> pure $ inject $ SOP.I response
|
||||||
|
|
||||||
|
mockMutationHandler :: I.IORef MockConfig -> I.IORef (Maybe AgentRequest) -> I.IORef (Maybe API.Config) -> API.SourceName -> API.Config -> API.MutationRequest -> Handler (Union API.MutationResponses)
|
||||||
|
mockMutationHandler mcfg mRecordedRequest mRecordedRequestConfig _sourceName requestConfig mutation = liftIO $ do
|
||||||
|
handler <- fmap _mutationResponse $ I.readIORef mcfg
|
||||||
|
I.writeIORef mRecordedRequest (Just $ Mutation mutation)
|
||||||
|
I.writeIORef mRecordedRequestConfig (Just requestConfig)
|
||||||
|
case handler mutation of
|
||||||
|
Left err -> pure $ inject $ SOP.I err
|
||||||
|
Right response -> pure $ inject $ SOP.I response
|
||||||
|
|
||||||
-- Returns an empty explain response for now
|
-- Returns an empty explain response for now
|
||||||
explainHandler :: API.SourceName -> API.Config -> API.QueryRequest -> Handler API.ExplainResponse
|
explainHandler :: API.SourceName -> API.Config -> API.QueryRequest -> Handler API.ExplainResponse
|
||||||
explainHandler _sourceName _queryConfig _query = pure $ API.ExplainResponse [] ""
|
explainHandler _sourceName _queryConfig _query = pure $ API.ExplainResponse [] ""
|
||||||
|
|
||||||
-- Returns an empty mutation response for now
|
|
||||||
mutationsHandler :: API.SourceName -> API.Config -> API.MutationRequest -> Handler (Union API.MutationResponses)
|
|
||||||
mutationsHandler _ _ _ = pure . inject . SOP.I $ API.MutationResponse []
|
|
||||||
|
|
||||||
healthcheckHandler :: Maybe API.SourceName -> Maybe API.Config -> Handler NoContent
|
healthcheckHandler :: Maybe API.SourceName -> Maybe API.Config -> Handler NoContent
|
||||||
healthcheckHandler _sourceName _config = pure NoContent
|
healthcheckHandler _sourceName _config = pure NoContent
|
||||||
|
|
||||||
@ -786,13 +806,13 @@ metricsHandler = pure "# NOTE: Metrics would go here."
|
|||||||
rawHandler :: API.SourceName -> API.Config -> API.RawRequest -> Handler API.RawResponse
|
rawHandler :: API.SourceName -> API.Config -> API.RawRequest -> Handler API.RawResponse
|
||||||
rawHandler _ _ _ = pure $ API.RawResponse [] -- NOTE: Raw query response would go here.
|
rawHandler _ _ _ = pure $ API.RawResponse [] -- NOTE: Raw query response would go here.
|
||||||
|
|
||||||
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> I.IORef (Maybe API.Config) -> Server API.Api
|
dcMockableServer :: I.IORef MockConfig -> I.IORef (Maybe AgentRequest) -> I.IORef (Maybe API.Config) -> Server API.Api
|
||||||
dcMockableServer mcfg mquery mQueryConfig =
|
dcMockableServer mcfg mRecordedRequest mRecordedRequestConfig =
|
||||||
mockCapabilitiesHandler mcfg
|
mockCapabilitiesHandler mcfg
|
||||||
:<|> mockSchemaHandler mcfg mQueryConfig
|
:<|> mockSchemaHandler mcfg mRecordedRequest mRecordedRequestConfig
|
||||||
:<|> mockQueryHandler mcfg mquery mQueryConfig
|
:<|> mockQueryHandler mcfg mRecordedRequest mRecordedRequestConfig
|
||||||
:<|> explainHandler
|
:<|> explainHandler
|
||||||
:<|> mutationsHandler
|
:<|> mockMutationHandler mcfg mRecordedRequest mRecordedRequestConfig
|
||||||
:<|> healthcheckHandler
|
:<|> healthcheckHandler
|
||||||
:<|> metricsHandler
|
:<|> metricsHandler
|
||||||
:<|> rawHandler
|
:<|> rawHandler
|
||||||
@ -800,7 +820,7 @@ dcMockableServer mcfg mquery mQueryConfig =
|
|||||||
mockAgentPort :: Warp.Port
|
mockAgentPort :: Warp.Port
|
||||||
mockAgentPort = 65006
|
mockAgentPort = 65006
|
||||||
|
|
||||||
runMockServer :: I.IORef MockConfig -> I.IORef (Maybe API.QueryRequest) -> I.IORef (Maybe API.Config) -> IO ()
|
runMockServer :: I.IORef MockConfig -> I.IORef (Maybe AgentRequest) -> I.IORef (Maybe API.Config) -> IO ()
|
||||||
runMockServer mcfg mquery mQueryConfig = do
|
runMockServer mcfg mRecordedRequest mRecordedRequestConfig = do
|
||||||
let app = serve (Proxy :: Proxy API.Api) $ dcMockableServer mcfg mquery mQueryConfig
|
let app = serve (Proxy :: Proxy API.Api) $ dcMockableServer mcfg mRecordedRequest mRecordedRequestConfig
|
||||||
Warp.run mockAgentPort app
|
Warp.run mockAgentPort app
|
||||||
|
@ -124,12 +124,13 @@ bqDBMutationPlan ::
|
|||||||
( MonadError E.QErr m
|
( MonadError E.QErr m
|
||||||
) =>
|
) =>
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
|
Env.Environment ->
|
||||||
Options.StringifyNumbers ->
|
Options.StringifyNumbers ->
|
||||||
SourceName ->
|
SourceName ->
|
||||||
SourceConfig 'BigQuery ->
|
SourceConfig 'BigQuery ->
|
||||||
MutationDB 'BigQuery Void (UnpreparedValue 'BigQuery) ->
|
MutationDB 'BigQuery Void (UnpreparedValue 'BigQuery) ->
|
||||||
m (DBStepInfo 'BigQuery)
|
m (DBStepInfo 'BigQuery)
|
||||||
bqDBMutationPlan _userInfo _stringifyNum _sourceName _sourceConfig _mrf =
|
bqDBMutationPlan _userInfo _environment _stringifyNum _sourceName _sourceConfig _mrf =
|
||||||
throw500 "mutations are not supported in BigQuery; this should be unreachable"
|
throw500 "mutations are not supported in BigQuery; this should be unreachable"
|
||||||
|
|
||||||
-- explain
|
-- explain
|
||||||
|
@ -1,22 +1,26 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Hasura.Backends.DataConnector.Adapter.Execute
|
module Hasura.Backends.DataConnector.Adapter.Execute
|
||||||
(
|
( DataConnectorPreparedQuery (..),
|
||||||
|
encodePreparedQueryToJsonText,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
import Data.Aeson qualified as J
|
import Data.Aeson qualified as J
|
||||||
|
import Data.ByteString.Lazy qualified as BL
|
||||||
import Data.Environment qualified as Env
|
import Data.Environment qualified as Env
|
||||||
|
import Data.Text.Encoding qualified as TE
|
||||||
import Data.Text.Extended (toTxt)
|
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 qualified as API
|
||||||
import Hasura.Backends.DataConnector.API.V0.ErrorResponse (ErrorResponse (..))
|
import Hasura.Backends.DataConnector.API.V0.ErrorResponse (ErrorResponse (..))
|
||||||
import Hasura.Backends.DataConnector.Adapter.ConfigTransform (transformSourceConfig)
|
import Hasura.Backends.DataConnector.Adapter.ConfigTransform (transformSourceConfig)
|
||||||
import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..))
|
import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..))
|
||||||
import Hasura.Backends.DataConnector.Agent.Client (AgentClientT)
|
import Hasura.Backends.DataConnector.Agent.Client (AgentClientT)
|
||||||
import Hasura.Backends.DataConnector.Plan qualified as DC
|
import Hasura.Backends.DataConnector.Plan.Common qualified as DC
|
||||||
|
import Hasura.Backends.DataConnector.Plan.MutationPlan qualified as DC
|
||||||
|
import Hasura.Backends.DataConnector.Plan.QueryPlan qualified as DC
|
||||||
import Hasura.Base.Error (Code (..), QErr, throw400, throw400WithDetail, throw500)
|
import Hasura.Base.Error (Code (..), QErr, throw400, throw400WithDetail, throw500)
|
||||||
import Hasura.EncJSON (EncJSON, encJFromBuilder, encJFromJValue)
|
import Hasura.EncJSON (EncJSON, encJFromBuilder, encJFromJValue)
|
||||||
import Hasura.GraphQL.Execute.Backend (BackendExecute (..), DBStepInfo (..), ExplainPlan (..))
|
import Hasura.GraphQL.Execute.Backend (BackendExecute (..), DBStepInfo (..), ExplainPlan (..))
|
||||||
@ -30,39 +34,59 @@ import Hasura.Tracing (MonadTrace)
|
|||||||
import Hasura.Tracing qualified as Tracing
|
import Hasura.Tracing qualified as Tracing
|
||||||
import Servant.Client.Core.HasClient ((//))
|
import Servant.Client.Core.HasClient ((//))
|
||||||
import Servant.Client.Generic (genericClient)
|
import Servant.Client.Generic (genericClient)
|
||||||
import Witch qualified
|
|
||||||
|
data DataConnectorPreparedQuery
|
||||||
|
= QueryRequest API.QueryRequest
|
||||||
|
| MutationRequest API.MutationRequest
|
||||||
|
|
||||||
|
encodePreparedQueryToJsonText :: DataConnectorPreparedQuery -> Text
|
||||||
|
encodePreparedQueryToJsonText = \case
|
||||||
|
QueryRequest req -> encodeToJsonText req
|
||||||
|
MutationRequest req -> encodeToJsonText req
|
||||||
|
|
||||||
|
encodeToJsonText :: J.ToJSON a => a -> Text
|
||||||
|
encodeToJsonText =
|
||||||
|
TE.decodeUtf8 . BL.toStrict . J.encode
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
instance BackendExecute 'DataConnector where
|
instance BackendExecute 'DataConnector where
|
||||||
type PreparedQuery 'DataConnector = API.QueryRequest
|
type PreparedQuery 'DataConnector = DataConnectorPreparedQuery
|
||||||
type MultiplexedQuery 'DataConnector = Void
|
type MultiplexedQuery 'DataConnector = Void
|
||||||
type ExecutionMonad 'DataConnector = AgentClientT (Tracing.TraceT (ExceptT QErr IO))
|
type ExecutionMonad 'DataConnector = AgentClientT (Tracing.TraceT (ExceptT QErr IO))
|
||||||
|
|
||||||
mkDBQueryPlan UserInfo {..} env sourceName sourceConfig ir = do
|
mkDBQueryPlan UserInfo {..} env sourceName sourceConfig ir = do
|
||||||
queryPlan@DC.QueryPlan {..} <- DC.mkPlan _uiSession sourceConfig ir
|
queryPlan@DC.Plan {..} <- DC.mkQueryPlan _uiSession sourceConfig ir
|
||||||
transformedSourceConfig <- transformSourceConfig sourceConfig [("$session", J.toJSON _uiSession), ("$env", J.toJSON env)] env
|
transformedSourceConfig <- transformSourceConfig sourceConfig [("$session", J.toJSON _uiSession), ("$env", J.toJSON env)] env
|
||||||
pure
|
pure
|
||||||
DBStepInfo
|
DBStepInfo
|
||||||
{ dbsiSourceName = sourceName,
|
{ dbsiSourceName = sourceName,
|
||||||
dbsiSourceConfig = transformedSourceConfig,
|
dbsiSourceConfig = transformedSourceConfig,
|
||||||
dbsiPreparedQuery = Just _qpRequest,
|
dbsiPreparedQuery = Just $ QueryRequest _pRequest,
|
||||||
dbsiAction = buildQueryAction sourceName transformedSourceConfig queryPlan
|
dbsiAction = buildQueryAction sourceName transformedSourceConfig queryPlan
|
||||||
}
|
}
|
||||||
|
|
||||||
mkDBQueryExplain fieldName UserInfo {..} sourceName sourceConfig ir = do
|
mkDBQueryExplain fieldName UserInfo {..} sourceName sourceConfig ir = do
|
||||||
queryPlan@DC.QueryPlan {..} <- DC.mkPlan _uiSession sourceConfig ir
|
queryPlan@DC.Plan {..} <- DC.mkQueryPlan _uiSession sourceConfig ir
|
||||||
transformedSourceConfig <- transformSourceConfig sourceConfig [("$session", J.toJSON _uiSession), ("$env", J.object [])] Env.emptyEnvironment
|
transformedSourceConfig <- transformSourceConfig sourceConfig [("$session", J.toJSON _uiSession), ("$env", J.object [])] Env.emptyEnvironment
|
||||||
pure $
|
pure $
|
||||||
mkAnyBackend @'DataConnector
|
mkAnyBackend @'DataConnector
|
||||||
DBStepInfo
|
DBStepInfo
|
||||||
{ dbsiSourceName = sourceName,
|
{ dbsiSourceName = sourceName,
|
||||||
dbsiSourceConfig = transformedSourceConfig,
|
dbsiSourceConfig = transformedSourceConfig,
|
||||||
dbsiPreparedQuery = Just _qpRequest,
|
dbsiPreparedQuery = Just $ QueryRequest _pRequest,
|
||||||
dbsiAction = buildExplainAction fieldName sourceName transformedSourceConfig queryPlan
|
dbsiAction = buildExplainAction fieldName sourceName transformedSourceConfig queryPlan
|
||||||
}
|
}
|
||||||
mkDBMutationPlan _ _ _ _ _ =
|
mkDBMutationPlan UserInfo {..} env _stringifyNum sourceName sourceConfig mutationDB = do
|
||||||
throw400 NotSupported "mkDBMutationPlan: not implemented for the Data Connector backend."
|
mutationPlan@DC.Plan {..} <- DC.mkMutationPlan _uiSession mutationDB
|
||||||
|
transformedSourceConfig <- transformSourceConfig sourceConfig [("$session", J.toJSON _uiSession), ("$env", J.toJSON env)] env
|
||||||
|
pure
|
||||||
|
DBStepInfo
|
||||||
|
{ dbsiSourceName = sourceName,
|
||||||
|
dbsiSourceConfig = transformedSourceConfig,
|
||||||
|
dbsiPreparedQuery = Just $ MutationRequest _pRequest,
|
||||||
|
dbsiAction = buildMutationAction sourceName transformedSourceConfig mutationPlan
|
||||||
|
}
|
||||||
mkLiveQuerySubscriptionPlan _ _ _ _ _ =
|
mkLiveQuerySubscriptionPlan _ _ _ _ _ =
|
||||||
throw400 NotSupported "mkLiveQuerySubscriptionPlan: not implemented for the Data Connector backend."
|
throw400 NotSupported "mkLiveQuerySubscriptionPlan: not implemented for the Data Connector backend."
|
||||||
mkDBStreamingSubscriptionPlan _ _ _ _ =
|
mkDBStreamingSubscriptionPlan _ _ _ _ =
|
||||||
@ -72,30 +96,28 @@ instance BackendExecute 'DataConnector where
|
|||||||
mkSubscriptionExplain _ =
|
mkSubscriptionExplain _ =
|
||||||
throw400 NotSupported "mkSubscriptionExplain: not implemented for the Data Connector backend."
|
throw400 NotSupported "mkSubscriptionExplain: not implemented for the Data Connector backend."
|
||||||
|
|
||||||
buildQueryAction :: (MonadIO m, MonadTrace m, MonadError QErr m) => RQL.SourceName -> SourceConfig -> DC.QueryPlan -> AgentClientT m EncJSON
|
buildQueryAction :: (MonadIO m, MonadTrace m, MonadError QErr m) => RQL.SourceName -> SourceConfig -> DC.Plan API.QueryRequest API.QueryResponse -> AgentClientT m EncJSON
|
||||||
buildQueryAction sourceName SourceConfig {..} DC.QueryPlan {..} = do
|
buildQueryAction sourceName SourceConfig {..} DC.Plan {..} = do
|
||||||
-- NOTE: Should this check occur during query construction in 'mkPlan'?
|
-- NOTE: Should this check occur during query construction in 'mkPlan'?
|
||||||
when (DC.queryHasRelations _qpRequest && isNothing (API._cRelationships _scCapabilities)) $
|
when (DC.queryHasRelations _pRequest && isNothing (API._cRelationships _scCapabilities)) $
|
||||||
throw400 NotSupported "Agents must provide their own dataloader."
|
throw400 NotSupported "Agents must provide their own dataloader."
|
||||||
let apiQueryRequest = Witch.into @API.QueryRequest _qpRequest
|
|
||||||
|
|
||||||
queryResponse <- queryGuard =<< (genericClient // API._query) (toTxt sourceName) _scConfig apiQueryRequest
|
queryResponse <- queryGuard =<< (genericClient // API._query) (toTxt sourceName) _scConfig _pRequest
|
||||||
reshapedResponse <- _qpResponseReshaper queryResponse
|
reshapedResponse <- _pResponseReshaper queryResponse
|
||||||
pure . encJFromBuilder $ J.fromEncoding reshapedResponse
|
pure . encJFromBuilder $ J.fromEncoding reshapedResponse
|
||||||
where
|
where
|
||||||
errorAction e = throw400WithDetail DataConnectorError (errorResponseSummary e) (_crDetails e)
|
errorAction e = throw400WithDetail DataConnectorError (API.errorResponseSummary e) (_crDetails e)
|
||||||
defaultAction = throw400 DataConnectorError "Unexpected data connector capabilities response - Unexpected Type"
|
defaultAction = throw400 DataConnectorError "Unexpected data connector query response - Unexpected Type"
|
||||||
queryGuard = queryCase defaultAction pure errorAction
|
queryGuard = API.queryCase defaultAction pure errorAction
|
||||||
|
|
||||||
-- Delegates the generation to the Agent's /explain endpoint if it has that capability,
|
-- Delegates the generation to the Agent's /explain endpoint if it has that capability,
|
||||||
-- otherwise, returns the IR sent to the agent.
|
-- otherwise, returns the IR sent to the agent.
|
||||||
buildExplainAction :: (MonadIO m, MonadTrace m, MonadError QErr m) => GQL.RootFieldAlias -> RQL.SourceName -> SourceConfig -> DC.QueryPlan -> AgentClientT m EncJSON
|
buildExplainAction :: (MonadIO m, MonadTrace m, MonadError QErr m) => GQL.RootFieldAlias -> RQL.SourceName -> SourceConfig -> DC.Plan API.QueryRequest API.QueryResponse -> AgentClientT m EncJSON
|
||||||
buildExplainAction fieldName sourceName SourceConfig {..} DC.QueryPlan {..} =
|
buildExplainAction fieldName sourceName SourceConfig {..} DC.Plan {..} =
|
||||||
case API._cExplain _scCapabilities of
|
case API._cExplain _scCapabilities of
|
||||||
Nothing -> pure . encJFromJValue . toExplainPlan fieldName $ _qpRequest
|
Nothing -> pure . encJFromJValue . toExplainPlan fieldName $ _pRequest
|
||||||
Just API.ExplainCapabilities -> do
|
Just API.ExplainCapabilities -> do
|
||||||
let apiQueryRequest = Witch.into @API.QueryRequest _qpRequest
|
explainResponse <- (genericClient // API._explain) (toTxt sourceName) _scConfig _pRequest
|
||||||
explainResponse <- (genericClient // API._explain) (toTxt sourceName) _scConfig apiQueryRequest
|
|
||||||
pure . encJFromJValue $
|
pure . encJFromJValue $
|
||||||
ExplainPlan
|
ExplainPlan
|
||||||
fieldName
|
fieldName
|
||||||
@ -104,4 +126,14 @@ buildExplainAction fieldName sourceName SourceConfig {..} DC.QueryPlan {..} =
|
|||||||
|
|
||||||
toExplainPlan :: GQL.RootFieldAlias -> API.QueryRequest -> ExplainPlan
|
toExplainPlan :: GQL.RootFieldAlias -> API.QueryRequest -> ExplainPlan
|
||||||
toExplainPlan fieldName queryRequest =
|
toExplainPlan fieldName queryRequest =
|
||||||
ExplainPlan fieldName (Just "") (Just [DC.renderQuery $ queryRequest])
|
ExplainPlan fieldName (Just "") (Just [encodeToJsonText queryRequest])
|
||||||
|
|
||||||
|
buildMutationAction :: (MonadIO m, MonadTrace m, MonadError QErr m) => RQL.SourceName -> SourceConfig -> DC.Plan API.MutationRequest API.MutationResponse -> AgentClientT m EncJSON
|
||||||
|
buildMutationAction sourceName SourceConfig {..} DC.Plan {..} = do
|
||||||
|
queryResponse <- mutationGuard =<< (genericClient // API._mutation) (toTxt sourceName) _scConfig _pRequest
|
||||||
|
reshapedResponse <- _pResponseReshaper queryResponse
|
||||||
|
pure . encJFromBuilder $ J.fromEncoding reshapedResponse
|
||||||
|
where
|
||||||
|
errorAction e = throw400WithDetail DataConnectorError (API.errorResponseSummary e) (_crDetails e)
|
||||||
|
defaultAction = throw400 DataConnectorError "Unexpected data connector mutations response - Unexpected Type"
|
||||||
|
mutationGuard = API.mutationCase defaultAction pure errorAction
|
||||||
|
@ -7,12 +7,10 @@ module Hasura.Backends.DataConnector.Adapter.Transport () where
|
|||||||
import Control.Exception.Safe (throwIO)
|
import Control.Exception.Safe (throwIO)
|
||||||
import Data.Aeson qualified as J
|
import Data.Aeson qualified as J
|
||||||
import Data.Text.Extended ((<>>))
|
import Data.Text.Extended ((<>>))
|
||||||
import Hasura.Backends.DataConnector.API qualified as API
|
import Hasura.Backends.DataConnector.Adapter.Execute (DataConnectorPreparedQuery (..), encodePreparedQueryToJsonText)
|
||||||
import Hasura.Backends.DataConnector.Adapter.Execute ()
|
|
||||||
import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..))
|
import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..))
|
||||||
import Hasura.Backends.DataConnector.Agent.Client (AgentClientContext (..), AgentClientT, runAgentClientT)
|
import Hasura.Backends.DataConnector.Agent.Client (AgentClientContext (..), AgentClientT, runAgentClientT)
|
||||||
import Hasura.Backends.DataConnector.Plan qualified as DC
|
import Hasura.Base.Error (QErr)
|
||||||
import Hasura.Base.Error (Code (NotSupported), QErr, throw400)
|
|
||||||
import Hasura.EncJSON (EncJSON)
|
import Hasura.EncJSON (EncJSON)
|
||||||
import Hasura.GraphQL.Execute.Backend (DBStepInfo (..))
|
import Hasura.GraphQL.Execute.Backend (DBStepInfo (..))
|
||||||
import Hasura.GraphQL.Logging qualified as HGL
|
import Hasura.GraphQL.Logging qualified as HGL
|
||||||
@ -31,8 +29,7 @@ import Hasura.Tracing qualified as Tracing
|
|||||||
instance BackendTransport 'DataConnector where
|
instance BackendTransport 'DataConnector where
|
||||||
runDBQuery = runDBQuery'
|
runDBQuery = runDBQuery'
|
||||||
runDBQueryExplain = runDBQueryExplain'
|
runDBQueryExplain = runDBQueryExplain'
|
||||||
runDBMutation _ _ _ _ _ _ _ _ =
|
runDBMutation = runDBMutation'
|
||||||
throw400 NotSupported "runDBMutation: not implemented for the Data Connector backend."
|
|
||||||
runDBStreamingSubscription _ _ _ =
|
runDBStreamingSubscription _ _ _ =
|
||||||
liftIO . throwIO $ userError "runDBStreamingSubscription: not implemented for the Data Connector backend."
|
liftIO . throwIO $ userError "runDBStreamingSubscription: not implemented for the Data Connector backend."
|
||||||
runDBSubscription _ _ _ =
|
runDBSubscription _ _ _ =
|
||||||
@ -51,7 +48,7 @@ runDBQuery' ::
|
|||||||
Logger Hasura ->
|
Logger Hasura ->
|
||||||
SourceConfig ->
|
SourceConfig ->
|
||||||
AgentClientT (Tracing.TraceT (ExceptT QErr IO)) a ->
|
AgentClientT (Tracing.TraceT (ExceptT QErr IO)) a ->
|
||||||
Maybe API.QueryRequest ->
|
Maybe DataConnectorPreparedQuery ->
|
||||||
m (DiffTime, a)
|
m (DiffTime, a)
|
||||||
runDBQuery' requestId query fieldName _userInfo logger SourceConfig {..} action queryRequest = do
|
runDBQuery' requestId query fieldName _userInfo logger SourceConfig {..} action queryRequest = do
|
||||||
void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId
|
void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId
|
||||||
@ -64,13 +61,13 @@ runDBQuery' requestId query fieldName _userInfo logger SourceConfig {..} action
|
|||||||
mkQueryLog ::
|
mkQueryLog ::
|
||||||
GQLReqUnparsed ->
|
GQLReqUnparsed ->
|
||||||
RootFieldAlias ->
|
RootFieldAlias ->
|
||||||
Maybe API.QueryRequest ->
|
Maybe DataConnectorPreparedQuery ->
|
||||||
RequestId ->
|
RequestId ->
|
||||||
HGL.QueryLog
|
HGL.QueryLog
|
||||||
mkQueryLog gqlQuery fieldName maybeQuery requestId =
|
mkQueryLog gqlQuery fieldName maybeQuery requestId =
|
||||||
HGL.QueryLog
|
HGL.QueryLog
|
||||||
gqlQuery
|
gqlQuery
|
||||||
((\query -> (fieldName, HGL.GeneratedQuery (DC.renderQuery query) J.Null)) <$> maybeQuery)
|
((\query -> (fieldName, HGL.GeneratedQuery (encodePreparedQueryToJsonText query) J.Null)) <$> maybeQuery)
|
||||||
requestId
|
requestId
|
||||||
HGL.QueryLogKindDatabase
|
HGL.QueryLogKindDatabase
|
||||||
|
|
||||||
@ -85,3 +82,26 @@ runDBQueryExplain' (DBStepInfo _ SourceConfig {..} _ action) =
|
|||||||
. Tracing.ignoreTraceT
|
. Tracing.ignoreTraceT
|
||||||
. flip runAgentClientT (AgentClientContext nullLogger _scEndpoint _scManager _scTimeoutMicroseconds)
|
. flip runAgentClientT (AgentClientContext nullLogger _scEndpoint _scManager _scTimeoutMicroseconds)
|
||||||
$ action
|
$ action
|
||||||
|
|
||||||
|
runDBMutation' ::
|
||||||
|
( MonadIO m,
|
||||||
|
MonadError QErr m,
|
||||||
|
Tracing.MonadTrace m,
|
||||||
|
HGL.MonadQueryLog m
|
||||||
|
) =>
|
||||||
|
RequestId ->
|
||||||
|
GQLReqUnparsed ->
|
||||||
|
RootFieldAlias ->
|
||||||
|
UserInfo ->
|
||||||
|
Logger Hasura ->
|
||||||
|
SourceConfig ->
|
||||||
|
AgentClientT (Tracing.TraceT (ExceptT QErr IO)) a ->
|
||||||
|
Maybe DataConnectorPreparedQuery ->
|
||||||
|
m (DiffTime, a)
|
||||||
|
runDBMutation' requestId query fieldName _userInfo logger SourceConfig {..} action queryRequest = do
|
||||||
|
void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId
|
||||||
|
withElapsedTime
|
||||||
|
. Tracing.trace ("Data Connector backend mutation for root field " <>> fieldName)
|
||||||
|
. Tracing.interpTraceT (liftEitherM . liftIO . runExceptT)
|
||||||
|
. flip runAgentClientT (AgentClientContext logger _scEndpoint _scManager _scTimeoutMicroseconds)
|
||||||
|
$ action
|
||||||
|
@ -1,760 +0,0 @@
|
|||||||
module Hasura.Backends.DataConnector.Plan
|
|
||||||
( QueryPlan (..),
|
|
||||||
mkPlan,
|
|
||||||
renderQuery,
|
|
||||||
queryHasRelations,
|
|
||||||
)
|
|
||||||
where
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import Control.Monad.Trans.Writer.CPS qualified as CPS
|
|
||||||
import Data.Aeson qualified as J
|
|
||||||
import Data.Aeson.Encoding qualified as JE
|
|
||||||
import Data.Aeson.Types qualified as J
|
|
||||||
import Data.Bifunctor (Bifunctor (bimap))
|
|
||||||
import Data.ByteString qualified as BS
|
|
||||||
import Data.ByteString.Lazy qualified as BL
|
|
||||||
import Data.HashMap.Strict qualified as HashMap
|
|
||||||
import Data.List.NonEmpty qualified as NE
|
|
||||||
import Data.Semigroup (Min (..))
|
|
||||||
import Data.Text qualified as T
|
|
||||||
import Data.Text.Encoding qualified as TE
|
|
||||||
import Data.Text.Extended (toTxt, (<<>), (<>>))
|
|
||||||
import Hasura.Backends.DataConnector.API qualified as API
|
|
||||||
import Hasura.Backends.DataConnector.Adapter.Backend
|
|
||||||
import Hasura.Backends.DataConnector.Adapter.Types
|
|
||||||
import Hasura.Base.Error
|
|
||||||
import Hasura.Prelude
|
|
||||||
import Hasura.RQL.IR.BoolExp
|
|
||||||
import Hasura.RQL.IR.OrderBy
|
|
||||||
import Hasura.RQL.IR.Select
|
|
||||||
import Hasura.RQL.IR.Value
|
|
||||||
import Hasura.RQL.Types.Backend (SessionVarType)
|
|
||||||
import Hasura.RQL.Types.Column
|
|
||||||
import Hasura.RQL.Types.Common
|
|
||||||
import Hasura.RQL.Types.Relationships.Local (RelInfo (..))
|
|
||||||
import Hasura.SQL.Backend
|
|
||||||
import Hasura.SQL.Types (CollectableType (..))
|
|
||||||
import Hasura.Session
|
|
||||||
import Language.GraphQL.Draft.Syntax qualified as G
|
|
||||||
import Witch qualified
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
data QueryPlan = QueryPlan
|
|
||||||
{ _qpRequest :: API.QueryRequest,
|
|
||||||
_qpResponseReshaper :: forall m. (MonadError QErr m) => API.QueryResponse -> m J.Encoding
|
|
||||||
}
|
|
||||||
|
|
||||||
data FieldsAndAggregates = FieldsAndAggregates
|
|
||||||
{ _faaFields :: HashMap FieldName API.Field,
|
|
||||||
_faaAggregates :: HashMap FieldName API.Aggregate
|
|
||||||
}
|
|
||||||
deriving stock (Show, Eq)
|
|
||||||
|
|
||||||
instance Semigroup FieldsAndAggregates where
|
|
||||||
left <> right =
|
|
||||||
FieldsAndAggregates
|
|
||||||
(_faaFields left <> _faaFields right)
|
|
||||||
(_faaAggregates left <> _faaAggregates right)
|
|
||||||
|
|
||||||
instance Monoid FieldsAndAggregates where
|
|
||||||
mempty = FieldsAndAggregates mempty mempty
|
|
||||||
|
|
||||||
newtype FieldPrefix = FieldPrefix (Maybe FieldName)
|
|
||||||
deriving stock (Show, Eq)
|
|
||||||
|
|
||||||
instance Semigroup FieldPrefix where
|
|
||||||
(FieldPrefix Nothing) <> (FieldPrefix something) = FieldPrefix something
|
|
||||||
(FieldPrefix something) <> (FieldPrefix Nothing) = FieldPrefix something
|
|
||||||
(FieldPrefix (Just l)) <> (FieldPrefix (Just r)) = FieldPrefix . Just $ l <> "_" <> r
|
|
||||||
|
|
||||||
instance Monoid FieldPrefix where
|
|
||||||
mempty = FieldPrefix Nothing
|
|
||||||
|
|
||||||
noPrefix :: FieldPrefix
|
|
||||||
noPrefix = FieldPrefix Nothing
|
|
||||||
|
|
||||||
prefixWith :: FieldName -> FieldPrefix
|
|
||||||
prefixWith = FieldPrefix . Just
|
|
||||||
|
|
||||||
applyPrefix :: FieldPrefix -> FieldName -> FieldName
|
|
||||||
applyPrefix (FieldPrefix fieldNamePrefix) fieldName = maybe fieldName (\prefix -> prefix <> "_" <> fieldName) fieldNamePrefix
|
|
||||||
|
|
||||||
newtype TableRelationships = TableRelationships
|
|
||||||
{unTableRelationships :: HashMap API.TableName (HashMap API.RelationshipName API.Relationship)}
|
|
||||||
deriving stock (Eq, Show)
|
|
||||||
|
|
||||||
instance Semigroup TableRelationships where
|
|
||||||
(TableRelationships l) <> (TableRelationships r) = TableRelationships $ HashMap.unionWith HashMap.union l r
|
|
||||||
|
|
||||||
instance Monoid TableRelationships where
|
|
||||||
mempty = TableRelationships mempty
|
|
||||||
|
|
||||||
-- | Render a 'API.QueryRequest' as 'Text'.
|
|
||||||
--
|
|
||||||
-- NOTE: This is for logging and debug purposes only.
|
|
||||||
renderQuery :: API.QueryRequest -> Text
|
|
||||||
renderQuery =
|
|
||||||
TE.decodeUtf8 . BL.toStrict . J.encode
|
|
||||||
|
|
||||||
-- | Map a 'QueryDB 'DataConnector' term into a 'Plan'
|
|
||||||
mkPlan ::
|
|
||||||
forall m.
|
|
||||||
MonadError QErr m =>
|
|
||||||
SessionVariables ->
|
|
||||||
SourceConfig ->
|
|
||||||
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
m QueryPlan
|
|
||||||
mkPlan session (SourceConfig {}) ir = do
|
|
||||||
queryRequest <- translateQueryDB ir
|
|
||||||
pure $ QueryPlan queryRequest (reshapeResponseToQueryShape ir)
|
|
||||||
where
|
|
||||||
translateQueryDB ::
|
|
||||||
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
m API.QueryRequest
|
|
||||||
translateQueryDB =
|
|
||||||
\case
|
|
||||||
QDBMultipleRows annSelect -> translateAnnSelectToQueryRequest (translateAnnFields noPrefix) annSelect
|
|
||||||
QDBSingleRow annSelect -> translateAnnSelectToQueryRequest (translateAnnFields noPrefix) annSelect
|
|
||||||
QDBAggregation annSelect -> translateAnnSelectToQueryRequest translateTableAggregateFields annSelect
|
|
||||||
|
|
||||||
translateAnnSelectToQueryRequest ::
|
|
||||||
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) ->
|
|
||||||
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
|
|
||||||
m API.QueryRequest
|
|
||||||
translateAnnSelectToQueryRequest translateFieldsAndAggregates selectG = do
|
|
||||||
tableName <- extractTableName selectG
|
|
||||||
(query, (TableRelationships tableRelationships)) <- CPS.runWriterT (translateAnnSelect translateFieldsAndAggregates tableName selectG)
|
|
||||||
let apiTableRelationships = uncurry API.TableRelationships <$> HashMap.toList tableRelationships
|
|
||||||
pure $
|
|
||||||
API.QueryRequest
|
|
||||||
{ _qrTable = tableName,
|
|
||||||
_qrTableRelationships = apiTableRelationships,
|
|
||||||
_qrQuery = query
|
|
||||||
}
|
|
||||||
|
|
||||||
extractTableName :: AnnSelectG 'DataConnector fieldsType valueType -> m API.TableName
|
|
||||||
extractTableName selectG =
|
|
||||||
case _asnFrom selectG of
|
|
||||||
FromTable tn -> pure $ Witch.from tn
|
|
||||||
FromIdentifier _ -> throw400 NotSupported "AnnSelectG: FromIdentifier not supported"
|
|
||||||
FromFunction {} -> throw400 NotSupported "AnnSelectG: FromFunction not supported"
|
|
||||||
|
|
||||||
recordTableRelationship :: API.TableName -> API.RelationshipName -> API.Relationship -> CPS.WriterT TableRelationships m ()
|
|
||||||
recordTableRelationship sourceTableName relationshipName relationship =
|
|
||||||
CPS.tell . TableRelationships $ HashMap.singleton sourceTableName (HashMap.singleton relationshipName relationship)
|
|
||||||
|
|
||||||
translateAnnSelect ::
|
|
||||||
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) ->
|
|
||||||
API.TableName ->
|
|
||||||
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m API.Query
|
|
||||||
translateAnnSelect translateFieldsAndAggregates tableName selectG = do
|
|
||||||
FieldsAndAggregates {..} <- translateFieldsAndAggregates tableName (_asnFields selectG)
|
|
||||||
let whereClauseWithPermissions =
|
|
||||||
case _saWhere (_asnArgs selectG) of
|
|
||||||
Just expr -> BoolAnd [expr, _tpFilter (_asnPerm selectG)]
|
|
||||||
Nothing -> _tpFilter (_asnPerm selectG)
|
|
||||||
whereClause <- translateBoolExpToExpression tableName whereClauseWithPermissions
|
|
||||||
orderBy <- traverse (translateOrderBy tableName) (_saOrderBy $ _asnArgs selectG)
|
|
||||||
pure
|
|
||||||
API.Query
|
|
||||||
{ _qFields = mapFieldNameHashMap _faaFields,
|
|
||||||
_qAggregates = mapFieldNameHashMap _faaAggregates,
|
|
||||||
_qLimit =
|
|
||||||
fmap getMin $
|
|
||||||
foldMap
|
|
||||||
(fmap Min)
|
|
||||||
[ _saLimit (_asnArgs selectG),
|
|
||||||
_tpLimit (_asnPerm selectG)
|
|
||||||
],
|
|
||||||
_qOffset = fmap fromIntegral (_saOffset (_asnArgs selectG)),
|
|
||||||
_qWhere = whereClause,
|
|
||||||
_qOrderBy = orderBy
|
|
||||||
}
|
|
||||||
|
|
||||||
translateOrderBy ::
|
|
||||||
API.TableName ->
|
|
||||||
NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector)) ->
|
|
||||||
CPS.WriterT TableRelationships m API.OrderBy
|
|
||||||
translateOrderBy sourceTableName orderByItems = do
|
|
||||||
orderByElementsAndRelations <- for orderByItems \OrderByItemG {..} -> do
|
|
||||||
let orderDirection = maybe API.Ascending Witch.from obiType
|
|
||||||
translateOrderByElement sourceTableName orderDirection [] obiColumn
|
|
||||||
relations <- lift . mergeOrderByRelations $ snd <$> orderByElementsAndRelations
|
|
||||||
pure
|
|
||||||
API.OrderBy
|
|
||||||
{ _obRelations = relations,
|
|
||||||
_obElements = fst <$> orderByElementsAndRelations
|
|
||||||
}
|
|
||||||
|
|
||||||
translateOrderByElement ::
|
|
||||||
API.TableName ->
|
|
||||||
API.OrderDirection ->
|
|
||||||
[API.RelationshipName] ->
|
|
||||||
AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation)
|
|
||||||
translateOrderByElement sourceTableName orderDirection targetReversePath = \case
|
|
||||||
AOCColumn (ColumnInfo {..}) ->
|
|
||||||
pure
|
|
||||||
( API.OrderByElement
|
|
||||||
{ _obeTargetPath = reverse targetReversePath,
|
|
||||||
_obeTarget = API.OrderByColumn $ Witch.from ciColumn,
|
|
||||||
_obeOrderDirection = orderDirection
|
|
||||||
},
|
|
||||||
mempty
|
|
||||||
)
|
|
||||||
AOCObjectRelation relationshipInfo filterExp orderByElement -> do
|
|
||||||
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
|
||||||
(translatedOrderByElement, subOrderByRelations) <- translateOrderByElement _rTargetTable orderDirection (relationshipName : targetReversePath) orderByElement
|
|
||||||
|
|
||||||
targetTableWhereExp <- translateBoolExpToExpression _rTargetTable filterExp
|
|
||||||
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp subOrderByRelations)]
|
|
||||||
|
|
||||||
pure (translatedOrderByElement, orderByRelations)
|
|
||||||
AOCArrayAggregation relationshipInfo filterExp aggregateOrderByElement -> do
|
|
||||||
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
|
||||||
orderByTarget <- case aggregateOrderByElement of
|
|
||||||
AAOCount ->
|
|
||||||
pure API.OrderByStarCountAggregate
|
|
||||||
AAOOp aggFunctionTxt ColumnInfo {..} -> do
|
|
||||||
aggFunction <- lift $ translateSingleColumnAggregateFunction aggFunctionTxt
|
|
||||||
pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction $ Witch.from ciColumn
|
|
||||||
|
|
||||||
let translatedOrderByElement =
|
|
||||||
API.OrderByElement
|
|
||||||
{ _obeTargetPath = reverse (relationshipName : targetReversePath),
|
|
||||||
_obeTarget = orderByTarget,
|
|
||||||
_obeOrderDirection = orderDirection
|
|
||||||
}
|
|
||||||
|
|
||||||
targetTableWhereExp <- translateBoolExpToExpression _rTargetTable filterExp
|
|
||||||
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp mempty)]
|
|
||||||
pure (translatedOrderByElement, orderByRelations)
|
|
||||||
|
|
||||||
mergeOrderByRelations ::
|
|
||||||
Foldable f =>
|
|
||||||
f (HashMap API.RelationshipName API.OrderByRelation) ->
|
|
||||||
m (HashMap API.RelationshipName API.OrderByRelation)
|
|
||||||
mergeOrderByRelations orderByRelationsList =
|
|
||||||
foldM mergeMap mempty orderByRelationsList
|
|
||||||
where
|
|
||||||
mergeMap :: HashMap API.RelationshipName API.OrderByRelation -> HashMap API.RelationshipName API.OrderByRelation -> m (HashMap API.RelationshipName API.OrderByRelation)
|
|
||||||
mergeMap left right = foldM (\targetMap (relName, orderByRel) -> HashMap.alterF (maybe (pure $ Just orderByRel) (fmap Just . mergeOrderByRelation orderByRel)) relName targetMap) left $ HashMap.toList right
|
|
||||||
|
|
||||||
mergeOrderByRelation :: API.OrderByRelation -> API.OrderByRelation -> m API.OrderByRelation
|
|
||||||
mergeOrderByRelation right left =
|
|
||||||
if API._obrWhere left == API._obrWhere right
|
|
||||||
then do
|
|
||||||
mergedSubrelations <- mergeMap (API._obrSubrelations left) (API._obrSubrelations right)
|
|
||||||
pure $ API.OrderByRelation (API._obrWhere left) mergedSubrelations
|
|
||||||
else throw500 "mergeOrderByRelations: Differing filter expressions found for the same table"
|
|
||||||
|
|
||||||
recordTableRelationshipFromRelInfo ::
|
|
||||||
API.TableName ->
|
|
||||||
RelInfo 'DataConnector ->
|
|
||||||
CPS.WriterT TableRelationships m (API.RelationshipName, API.Relationship)
|
|
||||||
recordTableRelationshipFromRelInfo sourceTableName RelInfo {..} = do
|
|
||||||
let relationshipName = mkRelationshipName riName
|
|
||||||
let relationshipType = case riType of
|
|
||||||
ObjRel -> API.ObjectRelationship
|
|
||||||
ArrRel -> API.ArrayRelationship
|
|
||||||
let relationship =
|
|
||||||
API.Relationship
|
|
||||||
{ _rTargetTable = Witch.from riRTable,
|
|
||||||
_rRelationshipType = relationshipType,
|
|
||||||
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList riMapping
|
|
||||||
}
|
|
||||||
recordTableRelationship
|
|
||||||
sourceTableName
|
|
||||||
relationshipName
|
|
||||||
relationship
|
|
||||||
pure (relationshipName, relationship)
|
|
||||||
|
|
||||||
translateAnnFields ::
|
|
||||||
FieldPrefix ->
|
|
||||||
API.TableName ->
|
|
||||||
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m FieldsAndAggregates
|
|
||||||
translateAnnFields fieldNamePrefix sourceTableName fields = do
|
|
||||||
translatedFields <- traverse (traverse (translateAnnField sourceTableName)) fields
|
|
||||||
let translatedFields' = HashMap.fromList (mapMaybe (\(fieldName, field) -> (applyPrefix fieldNamePrefix fieldName,) <$> field) translatedFields)
|
|
||||||
pure $
|
|
||||||
FieldsAndAggregates
|
|
||||||
translatedFields'
|
|
||||||
mempty
|
|
||||||
|
|
||||||
translateAnnField ::
|
|
||||||
API.TableName ->
|
|
||||||
AnnFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m (Maybe API.Field)
|
|
||||||
translateAnnField sourceTableName = \case
|
|
||||||
AFColumn colField ->
|
|
||||||
-- TODO: make sure certain fields in colField are not in use, since we don't
|
|
||||||
-- support them
|
|
||||||
pure . Just $ API.ColumnField (Witch.from $ _acfColumn colField) (Witch.from . columnTypeToScalarType $ _acfType colField)
|
|
||||||
AFObjectRelation objRel -> do
|
|
||||||
let targetTable = Witch.from $ _aosTableFrom (_aarAnnSelect objRel)
|
|
||||||
let relationshipName = mkRelationshipName $ _aarRelationshipName objRel
|
|
||||||
FieldsAndAggregates {..} <- translateAnnFields noPrefix targetTable (_aosFields (_aarAnnSelect objRel))
|
|
||||||
whereClause <- translateBoolExpToExpression targetTable (_aosTableFilter (_aarAnnSelect objRel))
|
|
||||||
|
|
||||||
recordTableRelationship
|
|
||||||
sourceTableName
|
|
||||||
relationshipName
|
|
||||||
API.Relationship
|
|
||||||
{ _rTargetTable = targetTable,
|
|
||||||
_rRelationshipType = API.ObjectRelationship,
|
|
||||||
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping objRel)
|
|
||||||
}
|
|
||||||
|
|
||||||
pure . Just . API.RelField $
|
|
||||||
API.RelationshipField
|
|
||||||
relationshipName
|
|
||||||
( API.Query
|
|
||||||
{ _qFields = mapFieldNameHashMap _faaFields,
|
|
||||||
_qAggregates = mapFieldNameHashMap _faaAggregates,
|
|
||||||
_qWhere = whereClause,
|
|
||||||
_qLimit = Nothing,
|
|
||||||
_qOffset = Nothing,
|
|
||||||
_qOrderBy = Nothing
|
|
||||||
}
|
|
||||||
)
|
|
||||||
AFArrayRelation (ASSimple arrayRelationSelect) -> do
|
|
||||||
Just <$> translateArrayRelationSelect sourceTableName (translateAnnFields noPrefix) arrayRelationSelect
|
|
||||||
AFArrayRelation (ASAggregate arrayRelationSelect) ->
|
|
||||||
Just <$> translateArrayRelationSelect sourceTableName translateTableAggregateFields arrayRelationSelect
|
|
||||||
AFExpression _literal ->
|
|
||||||
-- We ignore literal text fields (we don't send them to the data connector agent)
|
|
||||||
-- and add them back to the response JSON when we reshape what the agent returns
|
|
||||||
-- to us
|
|
||||||
pure Nothing
|
|
||||||
|
|
||||||
translateArrayRelationSelect ::
|
|
||||||
API.TableName ->
|
|
||||||
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) ->
|
|
||||||
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector)) ->
|
|
||||||
CPS.WriterT TableRelationships m API.Field
|
|
||||||
translateArrayRelationSelect sourceTableName translateFieldsAndAggregates arrRel = do
|
|
||||||
targetTable <- lift $ extractTableName (_aarAnnSelect arrRel)
|
|
||||||
query <- translateAnnSelect translateFieldsAndAggregates targetTable (_aarAnnSelect arrRel)
|
|
||||||
let relationshipName = mkRelationshipName $ _aarRelationshipName arrRel
|
|
||||||
|
|
||||||
recordTableRelationship
|
|
||||||
sourceTableName
|
|
||||||
relationshipName
|
|
||||||
API.Relationship
|
|
||||||
{ _rTargetTable = targetTable,
|
|
||||||
_rRelationshipType = API.ArrayRelationship,
|
|
||||||
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping arrRel)
|
|
||||||
}
|
|
||||||
|
|
||||||
pure . API.RelField $
|
|
||||||
API.RelationshipField
|
|
||||||
relationshipName
|
|
||||||
query
|
|
||||||
|
|
||||||
translateTableAggregateFields ::
|
|
||||||
API.TableName ->
|
|
||||||
TableAggregateFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m FieldsAndAggregates
|
|
||||||
translateTableAggregateFields sourceTableName fields = do
|
|
||||||
mconcat <$> traverse (uncurry (translateTableAggregateField sourceTableName)) fields
|
|
||||||
|
|
||||||
translateTableAggregateField ::
|
|
||||||
API.TableName ->
|
|
||||||
FieldName ->
|
|
||||||
TableAggregateFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m FieldsAndAggregates
|
|
||||||
translateTableAggregateField sourceTableName fieldName = \case
|
|
||||||
TAFAgg aggregateFields -> do
|
|
||||||
let fieldNamePrefix = prefixWith fieldName
|
|
||||||
translatedAggregateFields <- lift $ mconcat <$> traverse (uncurry (translateAggregateField fieldNamePrefix)) aggregateFields
|
|
||||||
pure $
|
|
||||||
FieldsAndAggregates
|
|
||||||
mempty
|
|
||||||
translatedAggregateFields
|
|
||||||
TAFNodes _ fields ->
|
|
||||||
translateAnnFields (prefixWith fieldName) sourceTableName fields
|
|
||||||
TAFExp _txt ->
|
|
||||||
-- We ignore literal text fields (we don't send them to the data connector agent)
|
|
||||||
-- and add them back to the response JSON when we reshape what the agent returns
|
|
||||||
-- to us
|
|
||||||
pure mempty
|
|
||||||
|
|
||||||
translateAggregateField ::
|
|
||||||
FieldPrefix ->
|
|
||||||
FieldName ->
|
|
||||||
AggregateField 'DataConnector ->
|
|
||||||
m (HashMap FieldName API.Aggregate)
|
|
||||||
translateAggregateField fieldPrefix fieldName = \case
|
|
||||||
AFCount countAggregate ->
|
|
||||||
let aggregate =
|
|
||||||
case countAggregate of
|
|
||||||
StarCount -> API.StarCount
|
|
||||||
ColumnCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = False}
|
|
||||||
ColumnDistinctCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = True}
|
|
||||||
in pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) aggregate
|
|
||||||
AFOp AggregateOp {..} -> do
|
|
||||||
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
|
|
||||||
aggFunction <- translateSingleColumnAggregateFunction _aoOp
|
|
||||||
|
|
||||||
fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) ->
|
|
||||||
case columnField of
|
|
||||||
CFCol column _columnType ->
|
|
||||||
pure . Just $ (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn . API.SingleColumnAggregate aggFunction $ Witch.from column)
|
|
||||||
CFExp _txt ->
|
|
||||||
-- We ignore literal text fields (we don't send them to the data connector agent)
|
|
||||||
-- and add them back to the response JSON when we reshape what the agent returns
|
|
||||||
-- to us
|
|
||||||
pure Nothing
|
|
||||||
AFExp _txt ->
|
|
||||||
-- We ignore literal text fields (we don't send them to the data connector agent)
|
|
||||||
-- and add them back to the response JSON when we reshape what the agent returns
|
|
||||||
-- to us
|
|
||||||
pure mempty
|
|
||||||
|
|
||||||
translateSingleColumnAggregateFunction :: Text -> m API.SingleColumnAggregateFunction
|
|
||||||
translateSingleColumnAggregateFunction functionName =
|
|
||||||
fmap API.SingleColumnAggregateFunction (G.mkName functionName)
|
|
||||||
`onNothing` throw500 ("translateSingleColumnAggregateFunction: Invalid aggregate function encountered: " <> functionName)
|
|
||||||
|
|
||||||
prepareLiterals ::
|
|
||||||
UnpreparedValue 'DataConnector ->
|
|
||||||
m Literal
|
|
||||||
prepareLiterals (UVLiteral literal) = pure $ literal
|
|
||||||
prepareLiterals (UVParameter _ e) = pure (ValueLiteral (columnTypeToScalarType $ cvType e) (cvValue e))
|
|
||||||
prepareLiterals UVSession = throw400 NotSupported "prepareLiterals: UVSession"
|
|
||||||
prepareLiterals (UVSessionVar sessionVarType sessionVar) = do
|
|
||||||
textValue <-
|
|
||||||
getSessionVariableValue sessionVar session
|
|
||||||
`onNothing` throw400 NotSupported ("prepareLiterals: session var not found: " <>> sessionVar)
|
|
||||||
parseSessionVariable sessionVar sessionVarType textValue
|
|
||||||
|
|
||||||
parseSessionVariable :: SessionVariable -> SessionVarType 'DataConnector -> Text -> m Literal
|
|
||||||
parseSessionVariable varName varType varValue = do
|
|
||||||
case varType of
|
|
||||||
CollectableTypeScalar scalarType@(ScalarType customTypeName _) ->
|
|
||||||
parseCustomValue scalarType (customTypeName <> " JSON value")
|
|
||||||
CollectableTypeArray scalarType@(ScalarType customTypeName _) ->
|
|
||||||
parseCustomArray scalarType ("JSON array of " <> customTypeName <> " JSON values")
|
|
||||||
where
|
|
||||||
parseCustomValue :: ScalarType -> Text -> m Literal
|
|
||||||
parseCustomValue scalarType description =
|
|
||||||
case scalarType of
|
|
||||||
ScalarType _ (Just GraphQLString) ->
|
|
||||||
-- Special case for string: uses literal session variable value rather than trying to parse a JSON string
|
|
||||||
pure . ValueLiteral scalarType $ J.String varValue
|
|
||||||
_ ->
|
|
||||||
parseValue' (parseValue scalarType) (ValueLiteral scalarType) description
|
|
||||||
|
|
||||||
parseCustomArray :: ScalarType -> Text -> m Literal
|
|
||||||
parseCustomArray scalarType =
|
|
||||||
parseValue' parser (ArrayLiteral scalarType)
|
|
||||||
where
|
|
||||||
parser :: (J.Value -> J.Parser [J.Value])
|
|
||||||
parser = J.withArray "array of JSON values" (fmap toList . traverse (parseValue scalarType))
|
|
||||||
|
|
||||||
parseValue' :: (J.Value -> J.Parser a) -> (a -> Literal) -> Text -> m Literal
|
|
||||||
parseValue' parser toLiteral description =
|
|
||||||
toLiteral
|
|
||||||
<$> (J.eitherDecodeStrict' valValueBS >>= J.parseEither parser)
|
|
||||||
`onLeft` (\err -> throw400 ParseFailed ("Expected " <> description <> " for session variable " <> varName <<> ". " <> T.pack err))
|
|
||||||
|
|
||||||
valValueBS :: BS.ByteString
|
|
||||||
valValueBS = TE.encodeUtf8 varValue
|
|
||||||
|
|
||||||
translateBoolExpToExpression ::
|
|
||||||
API.TableName ->
|
|
||||||
AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m (Maybe API.Expression)
|
|
||||||
translateBoolExpToExpression sourceTableName boolExp = do
|
|
||||||
removeAlwaysTrueExpression <$> translateBoolExp sourceTableName boolExp
|
|
||||||
|
|
||||||
translateBoolExp ::
|
|
||||||
API.TableName ->
|
|
||||||
AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) ->
|
|
||||||
CPS.WriterT TableRelationships m API.Expression
|
|
||||||
translateBoolExp sourceTableName = \case
|
|
||||||
BoolAnd xs ->
|
|
||||||
mkIfZeroOrMany API.And . mapMaybe removeAlwaysTrueExpression <$> traverse (translateBoolExp sourceTableName) xs
|
|
||||||
BoolOr xs ->
|
|
||||||
mkIfZeroOrMany API.Or . mapMaybe removeAlwaysFalseExpression <$> traverse (translateBoolExp sourceTableName) xs
|
|
||||||
BoolNot x ->
|
|
||||||
API.Not <$> (translateBoolExp sourceTableName) x
|
|
||||||
BoolField (AVColumn c xs) ->
|
|
||||||
lift $ mkIfZeroOrMany API.And <$> traverse (translateOp (Witch.from $ ciColumn c) (Witch.from . columnTypeToScalarType $ ciType c)) xs
|
|
||||||
BoolField (AVRelationship relationshipInfo boolExp) -> do
|
|
||||||
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
|
||||||
API.Exists (API.RelatedTable relationshipName) <$> translateBoolExp _rTargetTable boolExp
|
|
||||||
BoolExists GExists {..} ->
|
|
||||||
let tableName = Witch.from _geTable
|
|
||||||
in API.Exists (API.UnrelatedTable tableName) <$> translateBoolExp tableName _geWhere
|
|
||||||
where
|
|
||||||
-- Makes an 'API.Expression' like 'API.And' if there is zero or many input expressions otherwise
|
|
||||||
-- just returns the singleton expression. This helps remove redundant 'API.And' etcs from the expression.
|
|
||||||
mkIfZeroOrMany :: ([API.Expression] -> API.Expression) -> [API.Expression] -> API.Expression
|
|
||||||
mkIfZeroOrMany mk = \case
|
|
||||||
[singleExp] -> singleExp
|
|
||||||
zeroOrManyExps -> mk zeroOrManyExps
|
|
||||||
|
|
||||||
removeAlwaysTrueExpression :: API.Expression -> Maybe API.Expression
|
|
||||||
removeAlwaysTrueExpression = \case
|
|
||||||
API.And [] -> Nothing
|
|
||||||
API.Not (API.Or []) -> Nothing
|
|
||||||
other -> Just other
|
|
||||||
|
|
||||||
removeAlwaysFalseExpression :: API.Expression -> Maybe API.Expression
|
|
||||||
removeAlwaysFalseExpression = \case
|
|
||||||
API.Or [] -> Nothing
|
|
||||||
API.Not (API.And []) -> Nothing
|
|
||||||
other -> Just other
|
|
||||||
|
|
||||||
translateOp ::
|
|
||||||
API.ColumnName ->
|
|
||||||
API.ScalarType ->
|
|
||||||
OpExpG 'DataConnector (UnpreparedValue 'DataConnector) ->
|
|
||||||
m API.Expression
|
|
||||||
translateOp columnName columnType opExp = do
|
|
||||||
preparedOpExp <- traverse prepareLiterals $ opExp
|
|
||||||
case preparedOpExp of
|
|
||||||
AEQ _ (ValueLiteral scalarType value) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar API.Equal value scalarType
|
|
||||||
AEQ _ (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for AEQ operator"
|
|
||||||
ANE _ (ValueLiteral scalarType value) ->
|
|
||||||
pure . API.Not $ mkApplyBinaryComparisonOperatorToScalar API.Equal value scalarType
|
|
||||||
ANE _ (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for ANE operator"
|
|
||||||
AGT (ValueLiteral scalarType value) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar API.GreaterThan value scalarType
|
|
||||||
AGT (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for AGT operator"
|
|
||||||
ALT (ValueLiteral scalarType value) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar API.LessThan value scalarType
|
|
||||||
ALT (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for ALT operator"
|
|
||||||
AGTE (ValueLiteral scalarType value) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar API.GreaterThanOrEqual value scalarType
|
|
||||||
AGTE (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for AGTE operator"
|
|
||||||
ALTE (ValueLiteral scalarType value) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar API.LessThanOrEqual value scalarType
|
|
||||||
ALTE (ArrayLiteral _scalarType _array) ->
|
|
||||||
throw400 NotSupported "Array literals not supported for ALTE operator"
|
|
||||||
ANISNULL ->
|
|
||||||
pure $ API.ApplyUnaryComparisonOperator API.IsNull currentComparisonColumn
|
|
||||||
ANISNOTNULL ->
|
|
||||||
pure $ API.Not (API.ApplyUnaryComparisonOperator API.IsNull currentComparisonColumn)
|
|
||||||
AIN literal -> pure $ inOperator literal
|
|
||||||
ANIN literal -> pure . API.Not $ inOperator literal
|
|
||||||
CEQ rootOrCurrentColumn ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.Equal rootOrCurrentColumn
|
|
||||||
CNE rootOrCurrentColumn ->
|
|
||||||
pure $ API.Not $ mkApplyBinaryComparisonOperatorToAnotherColumn API.Equal rootOrCurrentColumn
|
|
||||||
CGT rootOrCurrentColumn ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.GreaterThan rootOrCurrentColumn
|
|
||||||
CLT rootOrCurrentColumn ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.LessThan rootOrCurrentColumn
|
|
||||||
CGTE rootOrCurrentColumn ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.GreaterThanOrEqual rootOrCurrentColumn
|
|
||||||
CLTE rootOrCurrentColumn ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.LessThanOrEqual rootOrCurrentColumn
|
|
||||||
ALIKE _literal ->
|
|
||||||
throw400 NotSupported "The ALIKE operator is not supported by the Data Connector backend"
|
|
||||||
ANLIKE _literal ->
|
|
||||||
throw400 NotSupported "The ANLIKE operator is not supported by the Data Connector backend"
|
|
||||||
ACast _literal ->
|
|
||||||
throw400 NotSupported "The ACast operator is not supported by the Data Connector backend"
|
|
||||||
ABackendSpecific CustomBooleanOperator {..} -> case _cboRHS of
|
|
||||||
Nothing -> pure $ API.ApplyUnaryComparisonOperator (API.CustomUnaryComparisonOperator _cboName) currentComparisonColumn
|
|
||||||
Just (Left rootOrCurrentColumn) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn (API.CustomBinaryComparisonOperator _cboName) rootOrCurrentColumn
|
|
||||||
Just (Right (ValueLiteral scalarType value)) ->
|
|
||||||
pure $ mkApplyBinaryComparisonOperatorToScalar (API.CustomBinaryComparisonOperator _cboName) value scalarType
|
|
||||||
Just (Right (ArrayLiteral scalarType array)) ->
|
|
||||||
pure $ API.ApplyBinaryArrayComparisonOperator (API.CustomBinaryArrayComparisonOperator _cboName) currentComparisonColumn array (Witch.from scalarType)
|
|
||||||
where
|
|
||||||
currentComparisonColumn :: API.ComparisonColumn
|
|
||||||
currentComparisonColumn = API.ComparisonColumn API.CurrentTable columnName columnType
|
|
||||||
|
|
||||||
mkApplyBinaryComparisonOperatorToAnotherColumn :: API.BinaryComparisonOperator -> RootOrCurrentColumn 'DataConnector -> API.Expression
|
|
||||||
mkApplyBinaryComparisonOperatorToAnotherColumn operator (RootOrCurrentColumn rootOrCurrent otherColumnName) =
|
|
||||||
let columnPath = case rootOrCurrent of
|
|
||||||
IsRoot -> API.QueryTable
|
|
||||||
IsCurrent -> API.CurrentTable
|
|
||||||
in API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.AnotherColumn $ API.ComparisonColumn columnPath (Witch.from otherColumnName) columnType)
|
|
||||||
|
|
||||||
inOperator :: Literal -> API.Expression
|
|
||||||
inOperator literal =
|
|
||||||
let (values, scalarType) = case literal of
|
|
||||||
ArrayLiteral scalarType' array -> (array, scalarType')
|
|
||||||
ValueLiteral scalarType' value -> ([value], scalarType')
|
|
||||||
in API.ApplyBinaryArrayComparisonOperator API.In currentComparisonColumn values (Witch.from scalarType)
|
|
||||||
|
|
||||||
mkApplyBinaryComparisonOperatorToScalar :: API.BinaryComparisonOperator -> J.Value -> ScalarType -> API.Expression
|
|
||||||
mkApplyBinaryComparisonOperatorToScalar operator value scalarType =
|
|
||||||
API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.ScalarValue value (Witch.from scalarType))
|
|
||||||
|
|
||||||
-- | Validate if a 'API.QueryRequest' contains any relationships.
|
|
||||||
queryHasRelations :: API.QueryRequest -> Bool
|
|
||||||
queryHasRelations API.QueryRequest {..} = _qrTableRelationships /= mempty
|
|
||||||
|
|
||||||
data Cardinality
|
|
||||||
= Single
|
|
||||||
| Many
|
|
||||||
|
|
||||||
reshapeResponseToQueryShape ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
API.QueryResponse ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeResponseToQueryShape queryDb response =
|
|
||||||
case queryDb of
|
|
||||||
QDBMultipleRows simpleSelect -> reshapeSimpleSelectRows Many (_asnFields simpleSelect) response
|
|
||||||
QDBSingleRow simpleSelect -> reshapeSimpleSelectRows Single (_asnFields simpleSelect) response
|
|
||||||
QDBAggregation aggregateSelect -> reshapeTableAggregateFields (_asnFields aggregateSelect) response
|
|
||||||
|
|
||||||
reshapeSimpleSelectRows ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
Cardinality ->
|
|
||||||
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
API.QueryResponse ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeSimpleSelectRows cardinality fields API.QueryResponse {..} =
|
|
||||||
case cardinality of
|
|
||||||
Single ->
|
|
||||||
case rows of
|
|
||||||
[] -> pure $ J.toEncoding J.Null
|
|
||||||
[singleRow] -> reshapeAnnFields noPrefix fields singleRow
|
|
||||||
_multipleRows ->
|
|
||||||
throw500 "Data Connector agent returned multiple rows when only one was expected" -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
Many -> do
|
|
||||||
reshapedRows <- traverse (reshapeAnnFields noPrefix fields) rows
|
|
||||||
pure $ JE.list id reshapedRows
|
|
||||||
where
|
|
||||||
rows = fromMaybe mempty _qrRows
|
|
||||||
|
|
||||||
reshapeTableAggregateFields ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
TableAggregateFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
API.QueryResponse ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeTableAggregateFields tableAggregateFields API.QueryResponse {..} = do
|
|
||||||
reshapedFields <- forM tableAggregateFields $ \(fieldName@(FieldName fieldNameText), tableAggregateField) -> do
|
|
||||||
case tableAggregateField of
|
|
||||||
TAFAgg aggregateFields -> do
|
|
||||||
reshapedAggregateFields <- reshapeAggregateFields (prefixWith fieldName) aggregateFields responseAggregates
|
|
||||||
pure $ (fieldNameText, reshapedAggregateFields)
|
|
||||||
TAFNodes _ annFields -> do
|
|
||||||
reshapedRows <- traverse (reshapeAnnFields (prefixWith fieldName) annFields) responseRows
|
|
||||||
pure $ (fieldNameText, JE.list id reshapedRows)
|
|
||||||
TAFExp txt ->
|
|
||||||
pure $ (fieldNameText, JE.text txt)
|
|
||||||
pure $ encodeAssocListAsObject reshapedFields
|
|
||||||
where
|
|
||||||
responseRows = fromMaybe mempty _qrRows
|
|
||||||
responseAggregates = fromMaybe mempty _qrAggregates
|
|
||||||
|
|
||||||
reshapeAggregateFields ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
FieldPrefix ->
|
|
||||||
AggregateFields 'DataConnector ->
|
|
||||||
HashMap API.FieldName J.Value ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeAggregateFields fieldPrefix aggregateFields responseAggregates = do
|
|
||||||
reshapedFields <- forM aggregateFields $ \(fieldName@(FieldName fieldNameText), aggregateField) ->
|
|
||||||
case aggregateField of
|
|
||||||
AFCount _countAggregate -> do
|
|
||||||
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix fieldName
|
|
||||||
responseAggregateValue <-
|
|
||||||
HashMap.lookup fieldNameKey responseAggregates
|
|
||||||
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName fieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
pure (fieldNameText, J.toEncoding responseAggregateValue)
|
|
||||||
AFOp AggregateOp {..} -> do
|
|
||||||
reshapedColumnFields <- forM _aoFields $ \(columnFieldName@(FieldName columnFieldNameText), columnField) ->
|
|
||||||
case columnField of
|
|
||||||
CFCol _column _columnType -> do
|
|
||||||
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
|
|
||||||
let columnFieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix' columnFieldName
|
|
||||||
responseAggregateValue <-
|
|
||||||
HashMap.lookup columnFieldNameKey responseAggregates
|
|
||||||
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName columnFieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
pure (columnFieldNameText, J.toEncoding responseAggregateValue)
|
|
||||||
CFExp txt ->
|
|
||||||
pure (columnFieldNameText, JE.text txt)
|
|
||||||
pure (fieldNameText, encodeAssocListAsObject reshapedColumnFields)
|
|
||||||
AFExp txt ->
|
|
||||||
pure (fieldNameText, JE.text txt)
|
|
||||||
pure $ encodeAssocListAsObject reshapedFields
|
|
||||||
|
|
||||||
reshapeAnnFields ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
FieldPrefix ->
|
|
||||||
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
HashMap API.FieldName API.FieldValue ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeAnnFields fieldNamePrefix fields responseRow = do
|
|
||||||
reshapedFields <- forM fields $ \(fieldName@(FieldName fieldNameText), field) -> do
|
|
||||||
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldNamePrefix fieldName
|
|
||||||
let responseField =
|
|
||||||
HashMap.lookup fieldNameKey responseRow
|
|
||||||
`onNothing` throw500 ("Unable to find expected field " <> API.unFieldName fieldNameKey <> " in row returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
reshapedField <- reshapeField field responseField
|
|
||||||
pure (fieldNameText, reshapedField)
|
|
||||||
|
|
||||||
pure $ encodeAssocListAsObject reshapedFields
|
|
||||||
|
|
||||||
encodeAssocListAsObject :: [(Text, J.Encoding)] -> J.Encoding
|
|
||||||
encodeAssocListAsObject =
|
|
||||||
JE.dict
|
|
||||||
JE.text
|
|
||||||
id
|
|
||||||
(\fn -> foldr (uncurry fn))
|
|
||||||
|
|
||||||
reshapeField ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
AnnFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
|
||||||
m API.FieldValue -> -- This lookup is lazy (behind the monad) so that we can _not_ do it when we've got an AFExpression
|
|
||||||
m J.Encoding
|
|
||||||
reshapeField field responseFieldValue =
|
|
||||||
case field of
|
|
||||||
AFColumn _columnField -> do
|
|
||||||
columnFieldValue <- API.deserializeAsColumnFieldValue <$> responseFieldValue
|
|
||||||
pure $ J.toEncoding columnFieldValue
|
|
||||||
AFObjectRelation objectRelationField -> do
|
|
||||||
relationshipFieldValue <- API.deserializeAsRelationshipFieldValue <$> responseFieldValue
|
|
||||||
case relationshipFieldValue of
|
|
||||||
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
Right subqueryResponse ->
|
|
||||||
let fields = _aosFields $ _aarAnnSelect objectRelationField
|
|
||||||
in reshapeSimpleSelectRows Single fields subqueryResponse
|
|
||||||
AFArrayRelation (ASSimple simpleArrayRelationField) ->
|
|
||||||
reshapeAnnRelationSelect (reshapeSimpleSelectRows Many) simpleArrayRelationField =<< responseFieldValue
|
|
||||||
AFArrayRelation (ASAggregate aggregateArrayRelationField) ->
|
|
||||||
reshapeAnnRelationSelect reshapeTableAggregateFields aggregateArrayRelationField =<< responseFieldValue
|
|
||||||
AFExpression txt -> pure $ JE.text txt
|
|
||||||
|
|
||||||
reshapeAnnRelationSelect ::
|
|
||||||
MonadError QErr m =>
|
|
||||||
(Fields (fieldType (UnpreparedValue 'DataConnector)) -> API.QueryResponse -> m J.Encoding) ->
|
|
||||||
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector)) ->
|
|
||||||
API.FieldValue ->
|
|
||||||
m J.Encoding
|
|
||||||
reshapeAnnRelationSelect reshapeFields annRelationSelect fieldValue =
|
|
||||||
case API.deserializeAsRelationshipFieldValue fieldValue of
|
|
||||||
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
|
|
||||||
Right subqueryResponse ->
|
|
||||||
let annSimpleSelect = _aarAnnSelect annRelationSelect
|
|
||||||
in reshapeFields (_asnFields annSimpleSelect) subqueryResponse
|
|
||||||
|
|
||||||
mapFieldNameHashMap :: Eq v => HashMap FieldName v -> Maybe (HashMap API.FieldName v)
|
|
||||||
mapFieldNameHashMap = memptyToNothing . HashMap.mapKeys (API.FieldName . getFieldNameTxt)
|
|
||||||
|
|
||||||
memptyToNothing :: (Monoid m, Eq m) => m -> Maybe m
|
|
||||||
memptyToNothing m = if m == mempty then Nothing else Just m
|
|
||||||
|
|
||||||
mkRelationshipName :: RelName -> API.RelationshipName
|
|
||||||
mkRelationshipName relName = API.RelationshipName $ toTxt relName
|
|
363
server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs
Normal file
363
server/src-lib/Hasura/Backends/DataConnector/Plan/Common.hs
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
-- | This module contains Data Connector request/response planning code and utility
|
||||||
|
-- functions and types that are common across the different categories of requests
|
||||||
|
-- (ie queries, mutations, etc). It contains code and concepts that are independent
|
||||||
|
-- of these different categories.
|
||||||
|
--
|
||||||
|
-- Both 'Hasura.Backends.DataConnector.Plan.QueryPlan' and
|
||||||
|
-- 'Hasura.Backends.DataConnector.Plan.MutationPlan' use the contents of this module,
|
||||||
|
-- for example 'Hasura.Backends.DataConnector.Plan.QueryPlan.mkQueryPlan`.
|
||||||
|
module Hasura.Backends.DataConnector.Plan.Common
|
||||||
|
( Plan (..),
|
||||||
|
TableRelationships (..),
|
||||||
|
FieldPrefix,
|
||||||
|
noPrefix,
|
||||||
|
prefixWith,
|
||||||
|
applyPrefix,
|
||||||
|
Cardinality (..),
|
||||||
|
recordTableRelationship,
|
||||||
|
recordTableRelationshipFromRelInfo,
|
||||||
|
prepareLiteral,
|
||||||
|
translateBoolExpToExpression,
|
||||||
|
mkRelationshipName,
|
||||||
|
encodeAssocListAsObject,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Writer.CPS qualified as CPS
|
||||||
|
import Data.Aeson qualified as J
|
||||||
|
import Data.Aeson.Encoding qualified as JE
|
||||||
|
import Data.Aeson.Types qualified as J
|
||||||
|
import Data.Bifunctor (Bifunctor (bimap))
|
||||||
|
import Data.ByteString qualified as BS
|
||||||
|
import Data.Has (Has (modifier))
|
||||||
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
import Data.Text qualified as T
|
||||||
|
import Data.Text.Encoding qualified as TE
|
||||||
|
import Data.Text.Extended (toTxt, (<<>), (<>>))
|
||||||
|
import Hasura.Backends.DataConnector.API qualified as API
|
||||||
|
import Hasura.Backends.DataConnector.Adapter.Backend
|
||||||
|
import Hasura.Backends.DataConnector.Adapter.Types
|
||||||
|
import Hasura.Base.Error
|
||||||
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.BoolExp
|
||||||
|
import Hasura.RQL.IR.Value
|
||||||
|
import Hasura.RQL.Types.Backend (SessionVarType)
|
||||||
|
import Hasura.RQL.Types.Column
|
||||||
|
import Hasura.RQL.Types.Common
|
||||||
|
import Hasura.RQL.Types.Relationships.Local (RelInfo (..))
|
||||||
|
import Hasura.SQL.Backend
|
||||||
|
import Hasura.SQL.Types (CollectableType (..))
|
||||||
|
import Hasura.Session
|
||||||
|
import Witch qualified
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Represents a 'request' to be sent to a data connector agent ('_pRequest') and a function
|
||||||
|
-- that is capable of reshaping the response to that request into the final JSON form expected
|
||||||
|
-- to be returned by the GraphQL endpoint ('_pResponseReshaper').
|
||||||
|
data Plan request response = Plan
|
||||||
|
{ _pRequest :: request,
|
||||||
|
_pResponseReshaper :: forall m. (MonadError QErr m) => response -> m J.Encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | A monoidal data structure used to record Table Relationships encountered during request
|
||||||
|
-- translation. Used with 'recordTableRelationship'.
|
||||||
|
newtype TableRelationships = TableRelationships
|
||||||
|
{unTableRelationships :: HashMap API.TableName (HashMap API.RelationshipName API.Relationship)}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
instance Semigroup TableRelationships where
|
||||||
|
(TableRelationships l) <> (TableRelationships r) = TableRelationships $ HashMap.unionWith HashMap.union l r
|
||||||
|
|
||||||
|
instance Monoid TableRelationships where
|
||||||
|
mempty = TableRelationships mempty
|
||||||
|
|
||||||
|
-- | Records a table relationship encountered during request translation into the output of the current
|
||||||
|
-- 'CPS.WriterT'
|
||||||
|
recordTableRelationship ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
API.TableName ->
|
||||||
|
API.RelationshipName ->
|
||||||
|
API.Relationship ->
|
||||||
|
CPS.WriterT writerOutput m ()
|
||||||
|
recordTableRelationship sourceTableName relationshipName relationship =
|
||||||
|
let newRelationship = TableRelationships $ HashMap.singleton sourceTableName (HashMap.singleton relationshipName relationship)
|
||||||
|
in CPS.tell $ modifier (const newRelationship) mempty
|
||||||
|
|
||||||
|
recordTableRelationshipFromRelInfo ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
API.TableName ->
|
||||||
|
RelInfo 'DataConnector ->
|
||||||
|
CPS.WriterT writerOutput m (API.RelationshipName, API.Relationship)
|
||||||
|
recordTableRelationshipFromRelInfo sourceTableName RelInfo {..} = do
|
||||||
|
let relationshipName = mkRelationshipName riName
|
||||||
|
let relationshipType = case riType of
|
||||||
|
ObjRel -> API.ObjectRelationship
|
||||||
|
ArrRel -> API.ArrayRelationship
|
||||||
|
let relationship =
|
||||||
|
API.Relationship
|
||||||
|
{ _rTargetTable = Witch.from riRTable,
|
||||||
|
_rRelationshipType = relationshipType,
|
||||||
|
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList riMapping
|
||||||
|
}
|
||||||
|
recordTableRelationship
|
||||||
|
sourceTableName
|
||||||
|
relationshipName
|
||||||
|
relationship
|
||||||
|
pure (relationshipName, relationship)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Represents a potential prefix that can be applied to a field name, useful for
|
||||||
|
-- namespacing field names that may be otherwise duplicated.
|
||||||
|
newtype FieldPrefix = FieldPrefix (Maybe FieldName)
|
||||||
|
deriving stock (Show, Eq)
|
||||||
|
|
||||||
|
instance Semigroup FieldPrefix where
|
||||||
|
(FieldPrefix Nothing) <> (FieldPrefix something) = FieldPrefix something
|
||||||
|
(FieldPrefix something) <> (FieldPrefix Nothing) = FieldPrefix something
|
||||||
|
(FieldPrefix (Just l)) <> (FieldPrefix (Just r)) = FieldPrefix . Just $ l <> "_" <> r
|
||||||
|
|
||||||
|
instance Monoid FieldPrefix where
|
||||||
|
mempty = FieldPrefix Nothing
|
||||||
|
|
||||||
|
noPrefix :: FieldPrefix
|
||||||
|
noPrefix = FieldPrefix Nothing
|
||||||
|
|
||||||
|
prefixWith :: FieldName -> FieldPrefix
|
||||||
|
prefixWith = FieldPrefix . Just
|
||||||
|
|
||||||
|
applyPrefix :: FieldPrefix -> FieldName -> FieldName
|
||||||
|
applyPrefix (FieldPrefix fieldNamePrefix) fieldName = maybe fieldName (\prefix -> prefix <> "_" <> fieldName) fieldNamePrefix
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
data Cardinality
|
||||||
|
= Single
|
||||||
|
| Many
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
prepareLiteral ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
UnpreparedValue 'DataConnector ->
|
||||||
|
m Literal
|
||||||
|
prepareLiteral sessionVariables = \case
|
||||||
|
UVLiteral literal -> pure $ literal
|
||||||
|
UVParameter _ e -> pure (ValueLiteral (columnTypeToScalarType $ cvType e) (cvValue e))
|
||||||
|
UVSession -> throw400 NotSupported "prepareLiteral: UVSession"
|
||||||
|
UVSessionVar sessionVarType sessionVar -> do
|
||||||
|
textValue <-
|
||||||
|
getSessionVariableValue sessionVar sessionVariables
|
||||||
|
`onNothing` throw400 NotSupported ("prepareLiteral: session var not found: " <>> sessionVar)
|
||||||
|
parseSessionVariable sessionVar sessionVarType textValue
|
||||||
|
|
||||||
|
parseSessionVariable ::
|
||||||
|
forall m.
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariable ->
|
||||||
|
SessionVarType 'DataConnector ->
|
||||||
|
Text ->
|
||||||
|
m Literal
|
||||||
|
parseSessionVariable varName varType varValue = do
|
||||||
|
case varType of
|
||||||
|
CollectableTypeScalar scalarType@(ScalarType customTypeName _) ->
|
||||||
|
parseCustomValue scalarType (customTypeName <> " JSON value")
|
||||||
|
CollectableTypeArray scalarType@(ScalarType customTypeName _) ->
|
||||||
|
parseCustomArray scalarType ("JSON array of " <> customTypeName <> " JSON values")
|
||||||
|
where
|
||||||
|
parseCustomValue :: ScalarType -> Text -> m Literal
|
||||||
|
parseCustomValue scalarType description =
|
||||||
|
case scalarType of
|
||||||
|
ScalarType _ (Just GraphQLString) ->
|
||||||
|
-- Special case for string: uses literal session variable value rather than trying to parse a JSON string
|
||||||
|
pure . ValueLiteral scalarType $ J.String varValue
|
||||||
|
_ ->
|
||||||
|
parseValue' (parseValue scalarType) (ValueLiteral scalarType) description
|
||||||
|
|
||||||
|
parseCustomArray :: ScalarType -> Text -> m Literal
|
||||||
|
parseCustomArray scalarType =
|
||||||
|
parseValue' parser (ArrayLiteral scalarType)
|
||||||
|
where
|
||||||
|
parser :: (J.Value -> J.Parser [J.Value])
|
||||||
|
parser = J.withArray "array of JSON values" (fmap toList . traverse (parseValue scalarType))
|
||||||
|
|
||||||
|
parseValue' :: (J.Value -> J.Parser a) -> (a -> Literal) -> Text -> m Literal
|
||||||
|
parseValue' parser toLiteral description =
|
||||||
|
toLiteral
|
||||||
|
<$> (J.eitherDecodeStrict' valValueBS >>= J.parseEither parser)
|
||||||
|
`onLeft` (\err -> throw400 ParseFailed ("Expected " <> description <> " for session variable " <> varName <<> ". " <> T.pack err))
|
||||||
|
|
||||||
|
valValueBS :: BS.ByteString
|
||||||
|
valValueBS = TE.encodeUtf8 varValue
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
translateBoolExpToExpression ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m (Maybe API.Expression)
|
||||||
|
translateBoolExpToExpression sessionVariables sourceTableName boolExp = do
|
||||||
|
removeAlwaysTrueExpression <$> translateBoolExp sessionVariables sourceTableName boolExp
|
||||||
|
|
||||||
|
translateBoolExp ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnBoolExp 'DataConnector (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m API.Expression
|
||||||
|
translateBoolExp sessionVariables sourceTableName = \case
|
||||||
|
BoolAnd xs ->
|
||||||
|
mkIfZeroOrMany API.And . mapMaybe removeAlwaysTrueExpression <$> traverse (translateBoolExp' sourceTableName) xs
|
||||||
|
BoolOr xs ->
|
||||||
|
mkIfZeroOrMany API.Or . mapMaybe removeAlwaysFalseExpression <$> traverse (translateBoolExp' sourceTableName) xs
|
||||||
|
BoolNot x ->
|
||||||
|
API.Not <$> (translateBoolExp' sourceTableName) x
|
||||||
|
BoolField (AVColumn c xs) ->
|
||||||
|
lift $ mkIfZeroOrMany API.And <$> traverse (translateOp sessionVariables (Witch.from $ ciColumn c) (Witch.from . columnTypeToScalarType $ ciType c)) xs
|
||||||
|
BoolField (AVRelationship relationshipInfo boolExp) -> do
|
||||||
|
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
||||||
|
API.Exists (API.RelatedTable relationshipName) <$> translateBoolExp' _rTargetTable boolExp
|
||||||
|
BoolExists GExists {..} ->
|
||||||
|
let tableName = Witch.from _geTable
|
||||||
|
in API.Exists (API.UnrelatedTable tableName) <$> translateBoolExp' tableName _geWhere
|
||||||
|
where
|
||||||
|
translateBoolExp' = translateBoolExp sessionVariables
|
||||||
|
|
||||||
|
-- Makes an 'API.Expression' like 'API.And' if there is zero or many input expressions otherwise
|
||||||
|
-- just returns the singleton expression. This helps remove redundant 'API.And' etcs from the expression.
|
||||||
|
mkIfZeroOrMany :: ([API.Expression] -> API.Expression) -> [API.Expression] -> API.Expression
|
||||||
|
mkIfZeroOrMany mk = \case
|
||||||
|
[singleExp] -> singleExp
|
||||||
|
zeroOrManyExps -> mk zeroOrManyExps
|
||||||
|
|
||||||
|
removeAlwaysTrueExpression :: API.Expression -> Maybe API.Expression
|
||||||
|
removeAlwaysTrueExpression = \case
|
||||||
|
API.And [] -> Nothing
|
||||||
|
API.Not (API.Or []) -> Nothing
|
||||||
|
other -> Just other
|
||||||
|
|
||||||
|
removeAlwaysFalseExpression :: API.Expression -> Maybe API.Expression
|
||||||
|
removeAlwaysFalseExpression = \case
|
||||||
|
API.Or [] -> Nothing
|
||||||
|
API.Not (API.And []) -> Nothing
|
||||||
|
other -> Just other
|
||||||
|
|
||||||
|
translateOp ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.ColumnName ->
|
||||||
|
API.ScalarType ->
|
||||||
|
OpExpG 'DataConnector (UnpreparedValue 'DataConnector) ->
|
||||||
|
m API.Expression
|
||||||
|
translateOp sessionVariables columnName columnType opExp = do
|
||||||
|
preparedOpExp <- traverse (prepareLiteral sessionVariables) $ opExp
|
||||||
|
case preparedOpExp of
|
||||||
|
AEQ _ (ValueLiteral scalarType value) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar API.Equal value scalarType
|
||||||
|
AEQ _ (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for AEQ operator"
|
||||||
|
ANE _ (ValueLiteral scalarType value) ->
|
||||||
|
pure . API.Not $ mkApplyBinaryComparisonOperatorToScalar API.Equal value scalarType
|
||||||
|
ANE _ (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for ANE operator"
|
||||||
|
AGT (ValueLiteral scalarType value) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar API.GreaterThan value scalarType
|
||||||
|
AGT (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for AGT operator"
|
||||||
|
ALT (ValueLiteral scalarType value) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar API.LessThan value scalarType
|
||||||
|
ALT (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for ALT operator"
|
||||||
|
AGTE (ValueLiteral scalarType value) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar API.GreaterThanOrEqual value scalarType
|
||||||
|
AGTE (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for AGTE operator"
|
||||||
|
ALTE (ValueLiteral scalarType value) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar API.LessThanOrEqual value scalarType
|
||||||
|
ALTE (ArrayLiteral _scalarType _array) ->
|
||||||
|
throw400 NotSupported "Array literals not supported for ALTE operator"
|
||||||
|
ANISNULL ->
|
||||||
|
pure $ API.ApplyUnaryComparisonOperator API.IsNull currentComparisonColumn
|
||||||
|
ANISNOTNULL ->
|
||||||
|
pure $ API.Not (API.ApplyUnaryComparisonOperator API.IsNull currentComparisonColumn)
|
||||||
|
AIN literal -> pure $ inOperator literal
|
||||||
|
ANIN literal -> pure . API.Not $ inOperator literal
|
||||||
|
CEQ rootOrCurrentColumn ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.Equal rootOrCurrentColumn
|
||||||
|
CNE rootOrCurrentColumn ->
|
||||||
|
pure $ API.Not $ mkApplyBinaryComparisonOperatorToAnotherColumn API.Equal rootOrCurrentColumn
|
||||||
|
CGT rootOrCurrentColumn ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.GreaterThan rootOrCurrentColumn
|
||||||
|
CLT rootOrCurrentColumn ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.LessThan rootOrCurrentColumn
|
||||||
|
CGTE rootOrCurrentColumn ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.GreaterThanOrEqual rootOrCurrentColumn
|
||||||
|
CLTE rootOrCurrentColumn ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn API.LessThanOrEqual rootOrCurrentColumn
|
||||||
|
ALIKE _literal ->
|
||||||
|
throw400 NotSupported "The ALIKE operator is not supported by the Data Connector backend"
|
||||||
|
ANLIKE _literal ->
|
||||||
|
throw400 NotSupported "The ANLIKE operator is not supported by the Data Connector backend"
|
||||||
|
ACast _literal ->
|
||||||
|
throw400 NotSupported "The ACast operator is not supported by the Data Connector backend"
|
||||||
|
ABackendSpecific CustomBooleanOperator {..} -> case _cboRHS of
|
||||||
|
Nothing -> pure $ API.ApplyUnaryComparisonOperator (API.CustomUnaryComparisonOperator _cboName) currentComparisonColumn
|
||||||
|
Just (Left rootOrCurrentColumn) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToAnotherColumn (API.CustomBinaryComparisonOperator _cboName) rootOrCurrentColumn
|
||||||
|
Just (Right (ValueLiteral scalarType value)) ->
|
||||||
|
pure $ mkApplyBinaryComparisonOperatorToScalar (API.CustomBinaryComparisonOperator _cboName) value scalarType
|
||||||
|
Just (Right (ArrayLiteral scalarType array)) ->
|
||||||
|
pure $ API.ApplyBinaryArrayComparisonOperator (API.CustomBinaryArrayComparisonOperator _cboName) currentComparisonColumn array (Witch.from scalarType)
|
||||||
|
where
|
||||||
|
currentComparisonColumn :: API.ComparisonColumn
|
||||||
|
currentComparisonColumn = API.ComparisonColumn API.CurrentTable columnName columnType
|
||||||
|
|
||||||
|
mkApplyBinaryComparisonOperatorToAnotherColumn :: API.BinaryComparisonOperator -> RootOrCurrentColumn 'DataConnector -> API.Expression
|
||||||
|
mkApplyBinaryComparisonOperatorToAnotherColumn operator (RootOrCurrentColumn rootOrCurrent otherColumnName) =
|
||||||
|
let columnPath = case rootOrCurrent of
|
||||||
|
IsRoot -> API.QueryTable
|
||||||
|
IsCurrent -> API.CurrentTable
|
||||||
|
in API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.AnotherColumn $ API.ComparisonColumn columnPath (Witch.from otherColumnName) columnType)
|
||||||
|
|
||||||
|
inOperator :: Literal -> API.Expression
|
||||||
|
inOperator literal =
|
||||||
|
let (values, scalarType) = case literal of
|
||||||
|
ArrayLiteral scalarType' array -> (array, scalarType')
|
||||||
|
ValueLiteral scalarType' value -> ([value], scalarType')
|
||||||
|
in API.ApplyBinaryArrayComparisonOperator API.In currentComparisonColumn values (Witch.from scalarType)
|
||||||
|
|
||||||
|
mkApplyBinaryComparisonOperatorToScalar :: API.BinaryComparisonOperator -> J.Value -> ScalarType -> API.Expression
|
||||||
|
mkApplyBinaryComparisonOperatorToScalar operator value scalarType =
|
||||||
|
API.ApplyBinaryComparisonOperator operator currentComparisonColumn (API.ScalarValue value (Witch.from scalarType))
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
mkRelationshipName :: RelName -> API.RelationshipName
|
||||||
|
mkRelationshipName relName = API.RelationshipName $ toTxt relName
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
encodeAssocListAsObject :: [(Text, J.Encoding)] -> J.Encoding
|
||||||
|
encodeAssocListAsObject =
|
||||||
|
JE.dict
|
||||||
|
JE.text
|
||||||
|
id
|
||||||
|
(\fn -> foldr (uncurry fn))
|
@ -0,0 +1,259 @@
|
|||||||
|
module Hasura.Backends.DataConnector.Plan.MutationPlan
|
||||||
|
( mkMutationPlan,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Writer.CPS qualified as CPS
|
||||||
|
import Data.Aeson qualified as J
|
||||||
|
import Data.Aeson.Encoding qualified as JE
|
||||||
|
import Data.Has (Has, modifier)
|
||||||
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
import Data.Text.Extended (toTxt)
|
||||||
|
import Hasura.Backends.DataConnector.API qualified as API
|
||||||
|
import Hasura.Backends.DataConnector.Adapter.Types
|
||||||
|
import Hasura.Backends.DataConnector.Plan.Common
|
||||||
|
import Hasura.Backends.DataConnector.Plan.QueryPlan (reshapeAnnFields, translateAnnFields)
|
||||||
|
import Hasura.Base.Error (Code (..), QErr, throw400, throw500)
|
||||||
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.Delete
|
||||||
|
import Hasura.RQL.IR.Insert hiding (Single)
|
||||||
|
import Hasura.RQL.IR.Returning
|
||||||
|
import Hasura.RQL.IR.Root
|
||||||
|
import Hasura.RQL.IR.Select
|
||||||
|
import Hasura.RQL.IR.Update
|
||||||
|
import Hasura.RQL.IR.Value
|
||||||
|
import Hasura.RQL.Types.Column
|
||||||
|
import Hasura.RQL.Types.Common
|
||||||
|
import Hasura.SQL.Backend (BackendType (..))
|
||||||
|
import Hasura.Session (SessionVariables)
|
||||||
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
import Witch qualified
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
newtype TableInsertSchemas = TableInsertSchemas
|
||||||
|
{unTableInsertSchemas :: HashMap API.TableName (HashMap API.FieldName API.InsertFieldSchema)}
|
||||||
|
deriving stock (Eq, Show)
|
||||||
|
|
||||||
|
instance Semigroup TableInsertSchemas where
|
||||||
|
(TableInsertSchemas l) <> (TableInsertSchemas r) = TableInsertSchemas $ HashMap.unionWith HashMap.union l r
|
||||||
|
|
||||||
|
instance Monoid TableInsertSchemas where
|
||||||
|
mempty = TableInsertSchemas mempty
|
||||||
|
|
||||||
|
recordTableInsertSchema ::
|
||||||
|
( Has TableInsertSchemas writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
API.TableName ->
|
||||||
|
HashMap API.FieldName API.InsertFieldSchema ->
|
||||||
|
CPS.WriterT writerOutput m ()
|
||||||
|
recordTableInsertSchema tableName fieldSchemas =
|
||||||
|
let newTableSchema = TableInsertSchemas $ HashMap.singleton tableName fieldSchemas
|
||||||
|
in CPS.tell $ modifier (const newTableSchema) mempty
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
mkMutationPlan ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
MutationDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
m (Plan API.MutationRequest API.MutationResponse)
|
||||||
|
mkMutationPlan sessionVariables mutationDB = do
|
||||||
|
request <- translateMutationDB sessionVariables mutationDB
|
||||||
|
pure $ Plan request (reshapeResponseToMutationGqlShape mutationDB)
|
||||||
|
|
||||||
|
translateMutationDB ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
MutationDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
m API.MutationRequest
|
||||||
|
translateMutationDB sessionVariables = \case
|
||||||
|
MDBInsert insert -> do
|
||||||
|
(insertOperation, (tableRelationships, tableInsertSchemas)) <- CPS.runWriterT $ translateInsert sessionVariables insert
|
||||||
|
let apiTableRelationships = uncurry API.TableRelationships <$> HashMap.toList (unTableRelationships tableRelationships)
|
||||||
|
let apiTableInsertSchema = uncurry API.TableInsertSchema <$> HashMap.toList (unTableInsertSchemas tableInsertSchemas)
|
||||||
|
pure $
|
||||||
|
API.MutationRequest
|
||||||
|
{ _mrTableRelationships = apiTableRelationships,
|
||||||
|
_mrInsertSchema = apiTableInsertSchema,
|
||||||
|
_mrOperations = [API.InsertOperation insertOperation]
|
||||||
|
}
|
||||||
|
MDBUpdate _update ->
|
||||||
|
throw400 NotSupported "translateMutationDB: update mutations not implemented for the Data Connector backend."
|
||||||
|
MDBDelete _delete ->
|
||||||
|
throw400 NotSupported "translateMutationDB: delete mutations not implemented for the Data Connector backend."
|
||||||
|
MDBFunction _returnsSet _select ->
|
||||||
|
throw400 NotSupported "translateMutationDB: function mutations not implemented for the Data Connector backend."
|
||||||
|
|
||||||
|
translateInsert ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
AnnotatedInsert 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT (TableRelationships, TableInsertSchemas) m API.InsertMutationOperation
|
||||||
|
translateInsert sessionVariables AnnotatedInsert {..} = do
|
||||||
|
rows <- traverse (translateInsertRow sessionVariables tableName _aiTableColumns _aiPresetValues) _aiInsertObject
|
||||||
|
postInsertCheck <- translateBoolExpToExpression sessionVariables tableName insertCheckCondition
|
||||||
|
returningFields <- translateMutationOutputToReturningFields sessionVariables tableName _aiOutput
|
||||||
|
pure $
|
||||||
|
API.InsertMutationOperation
|
||||||
|
{ API._imoTable = tableName,
|
||||||
|
API._imoRows = rows,
|
||||||
|
API._imoPostInsertCheck = postInsertCheck,
|
||||||
|
API._imoReturningFields = HashMap.mapKeys (API.FieldName . getFieldNameTxt) returningFields
|
||||||
|
}
|
||||||
|
where
|
||||||
|
tableName = Witch.from _aiTableName
|
||||||
|
insertCheckCondition = fst _aiCheckCondition
|
||||||
|
AnnotatedInsertData {..} = _aiData
|
||||||
|
|
||||||
|
translateInsertRow ::
|
||||||
|
( Has TableInsertSchemas writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
[ColumnInfo 'DataConnector] ->
|
||||||
|
HashMap ColumnName (UnpreparedValue 'DataConnector) ->
|
||||||
|
AnnotatedInsertRow 'DataConnector (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m API.RowObject
|
||||||
|
translateInsertRow sessionVariables tableName tableColumns defaultColumnValues insertRow = do
|
||||||
|
columnSchemasAndValues <- lift $ forM (HashMap.toList columnUnpreparedValues) $ \(columnName, columnValue) -> do
|
||||||
|
fieldName <-
|
||||||
|
case find (\ColumnInfo {..} -> ciColumn == columnName) tableColumns of
|
||||||
|
Just ColumnInfo {..} -> pure . API.FieldName $ G.unName ciName
|
||||||
|
Nothing -> throw500 $ "Can't find column " <> toTxt columnName <> " in table schema for " <> API.tableNameToText tableName
|
||||||
|
preparedLiteral <- prepareLiteral sessionVariables columnValue
|
||||||
|
|
||||||
|
(scalarType, value) <-
|
||||||
|
case preparedLiteral of
|
||||||
|
ValueLiteral scalarType value -> pure (scalarType, value)
|
||||||
|
ArrayLiteral _scalarType _values -> throw400 NotSupported "translateInsertRow: Array literals are not supported as column insert values"
|
||||||
|
|
||||||
|
let columnInsertSchema = API.ColumnInsertSchema (Witch.from columnName) (Witch.from scalarType)
|
||||||
|
pure (fieldName, columnInsertSchema, value)
|
||||||
|
|
||||||
|
let fieldSchemas =
|
||||||
|
columnSchemasAndValues
|
||||||
|
& fmap (\(fieldName, columnInsertSchema, _) -> (fieldName, API.ColumnInsert columnInsertSchema))
|
||||||
|
& HashMap.fromList
|
||||||
|
recordTableInsertSchema tableName fieldSchemas
|
||||||
|
|
||||||
|
let rowObject =
|
||||||
|
columnSchemasAndValues
|
||||||
|
& fmap (\(fieldName, _, value) -> (fieldName, API.mkColumnInsertFieldValue value))
|
||||||
|
& HashMap.fromList
|
||||||
|
& API.RowObject
|
||||||
|
|
||||||
|
pure rowObject
|
||||||
|
where
|
||||||
|
columnUnpreparedValues :: HashMap ColumnName (UnpreparedValue 'DataConnector)
|
||||||
|
columnUnpreparedValues = HashMap.union rowColumnValues defaultColumnValues
|
||||||
|
|
||||||
|
rowColumnValues :: HashMap ColumnName (UnpreparedValue 'DataConnector)
|
||||||
|
rowColumnValues =
|
||||||
|
insertRow
|
||||||
|
& fmap (\(AIColumn columnNameAndValue) -> columnNameAndValue)
|
||||||
|
& HashMap.fromList
|
||||||
|
|
||||||
|
translateMutationOutputToReturningFields ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
MutationOutputG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT (TableRelationships, TableInsertSchemas) m (HashMap FieldName API.Field)
|
||||||
|
translateMutationOutputToReturningFields sessionVariables tableName = \case
|
||||||
|
MOutSinglerowObject annFields ->
|
||||||
|
translateAnnFields sessionVariables noPrefix tableName annFields
|
||||||
|
MOutMultirowFields mutFields ->
|
||||||
|
HashMap.unions <$> traverse (uncurry $ translateMutField sessionVariables tableName) mutFields
|
||||||
|
|
||||||
|
translateMutField ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
FieldName ->
|
||||||
|
MutFldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT (TableRelationships, TableInsertSchemas) m (HashMap FieldName API.Field)
|
||||||
|
translateMutField sessionVariables tableName fieldName = \case
|
||||||
|
MCount ->
|
||||||
|
-- All mutation operations in a request return their affected rows count.
|
||||||
|
-- The count can just be added to the response JSON during agent response reshaping
|
||||||
|
pure mempty
|
||||||
|
MExp _text ->
|
||||||
|
-- We ignore literal text fields (we don't send them to the data connector agent)
|
||||||
|
-- and add them back to the response JSON when we reshape what the agent returns
|
||||||
|
-- to us
|
||||||
|
pure mempty
|
||||||
|
MRet annFields ->
|
||||||
|
translateAnnFields sessionVariables (prefixWith fieldName) tableName annFields
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
reshapeResponseToMutationGqlShape ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
MutationDB 'DataConnector Void v ->
|
||||||
|
API.MutationResponse ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeResponseToMutationGqlShape mutationDb API.MutationResponse {..} = do
|
||||||
|
mutationOperationResult <-
|
||||||
|
listToMaybe _mrOperationResults
|
||||||
|
`onNothing` throw500 "Unable to find expected mutation operation results"
|
||||||
|
|
||||||
|
mutationOutput <-
|
||||||
|
case mutationDb of
|
||||||
|
MDBInsert AnnotatedInsert {..} -> pure _aiOutput
|
||||||
|
MDBUpdate AnnotatedUpdateG {..} -> pure _auOutput
|
||||||
|
MDBDelete AnnDel {..} -> pure _adOutput
|
||||||
|
MDBFunction _returnsSet _select -> throw400 NotSupported "reshapeResponseToMutationGqlShape: function mutations not implemented for the Data Connector backend."
|
||||||
|
|
||||||
|
reshapeMutationOutput mutationOutput mutationOperationResult
|
||||||
|
|
||||||
|
reshapeMutationOutput ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
MutationOutputG 'DataConnector Void v ->
|
||||||
|
API.MutationOperationResults ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeMutationOutput mutationOutput mutationOperationResults =
|
||||||
|
case mutationOutput of
|
||||||
|
MOutSinglerowObject annFields -> reshapeReturningRows Single noPrefix annFields mutationOperationResults
|
||||||
|
MOutMultirowFields mutFields -> reshapeMutFields mutFields mutationOperationResults
|
||||||
|
|
||||||
|
reshapeReturningRows ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
Cardinality ->
|
||||||
|
FieldPrefix ->
|
||||||
|
AnnFieldsG 'DataConnector Void v ->
|
||||||
|
API.MutationOperationResults ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeReturningRows cardinality fieldNamePrefix annFields API.MutationOperationResults {..} =
|
||||||
|
case cardinality of
|
||||||
|
Single ->
|
||||||
|
case rows of
|
||||||
|
[] -> pure $ J.toEncoding J.Null
|
||||||
|
[singleRow] -> reshapeAnnFields fieldNamePrefix annFields singleRow
|
||||||
|
_multipleRows ->
|
||||||
|
throw500 "Data Connector agent returned multiple rows in a mutation operation result when only one was expected"
|
||||||
|
Many -> do
|
||||||
|
reshapedRows <- traverse (reshapeAnnFields fieldNamePrefix annFields) rows
|
||||||
|
pure $ JE.list id reshapedRows
|
||||||
|
where
|
||||||
|
rows = fromMaybe mempty _morReturning
|
||||||
|
|
||||||
|
reshapeMutFields ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
MutFldsG 'DataConnector Void v ->
|
||||||
|
API.MutationOperationResults ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeMutFields mutFields mutationOperationResults@API.MutationOperationResults {..} = do
|
||||||
|
reshapedMutFields <- forM mutFields $ \(fieldName@(FieldName fieldNameText), mutField) -> do
|
||||||
|
reshapedFieldValue <-
|
||||||
|
case mutField of
|
||||||
|
MCount -> pure $ JE.int _morAffectedRows
|
||||||
|
MExp literalText -> pure $ JE.text literalText
|
||||||
|
MRet annFields -> reshapeReturningRows Many (prefixWith fieldName) annFields mutationOperationResults
|
||||||
|
pure (fieldNameText, reshapedFieldValue)
|
||||||
|
|
||||||
|
pure $ encodeAssocListAsObject reshapedMutFields
|
554
server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs
Normal file
554
server/src-lib/Hasura/Backends/DataConnector/Plan/QueryPlan.hs
Normal file
@ -0,0 +1,554 @@
|
|||||||
|
module Hasura.Backends.DataConnector.Plan.QueryPlan
|
||||||
|
( mkQueryPlan,
|
||||||
|
queryHasRelations,
|
||||||
|
translateAnnFields,
|
||||||
|
reshapeAnnFields,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import Control.Monad.Trans.Writer.CPS qualified as CPS
|
||||||
|
import Data.Aeson qualified as J
|
||||||
|
import Data.Aeson.Encoding qualified as JE
|
||||||
|
import Data.Bifunctor (Bifunctor (bimap))
|
||||||
|
import Data.Has (Has)
|
||||||
|
import Data.HashMap.Strict qualified as HashMap
|
||||||
|
import Data.List.NonEmpty qualified as NE
|
||||||
|
import Data.Semigroup (Min (..))
|
||||||
|
import Hasura.Backends.DataConnector.API qualified as API
|
||||||
|
import Hasura.Backends.DataConnector.Adapter.Backend
|
||||||
|
import Hasura.Backends.DataConnector.Adapter.Types
|
||||||
|
import Hasura.Backends.DataConnector.Plan.Common
|
||||||
|
import Hasura.Base.Error
|
||||||
|
import Hasura.Prelude
|
||||||
|
import Hasura.RQL.IR.BoolExp
|
||||||
|
import Hasura.RQL.IR.OrderBy
|
||||||
|
import Hasura.RQL.IR.Select
|
||||||
|
import Hasura.RQL.IR.Value
|
||||||
|
import Hasura.RQL.Types.Column
|
||||||
|
import Hasura.RQL.Types.Common
|
||||||
|
import Hasura.SQL.Backend
|
||||||
|
import Hasura.Session
|
||||||
|
import Language.GraphQL.Draft.Syntax qualified as G
|
||||||
|
import Witch qualified
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
data FieldsAndAggregates = FieldsAndAggregates
|
||||||
|
{ _faaFields :: HashMap FieldName API.Field,
|
||||||
|
_faaAggregates :: HashMap FieldName API.Aggregate
|
||||||
|
}
|
||||||
|
deriving stock (Show, Eq)
|
||||||
|
|
||||||
|
instance Semigroup FieldsAndAggregates where
|
||||||
|
left <> right =
|
||||||
|
FieldsAndAggregates
|
||||||
|
(_faaFields left <> _faaFields right)
|
||||||
|
(_faaAggregates left <> _faaAggregates right)
|
||||||
|
|
||||||
|
instance Monoid FieldsAndAggregates where
|
||||||
|
mempty = FieldsAndAggregates mempty mempty
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Map a 'QueryDB 'DataConnector' term into a 'Plan'
|
||||||
|
mkQueryPlan ::
|
||||||
|
forall m.
|
||||||
|
MonadError QErr m =>
|
||||||
|
SessionVariables ->
|
||||||
|
SourceConfig ->
|
||||||
|
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
m (Plan API.QueryRequest API.QueryResponse)
|
||||||
|
mkQueryPlan sessionVariables (SourceConfig {}) ir = do
|
||||||
|
queryRequest <- translateQueryDB ir
|
||||||
|
pure $ Plan queryRequest (reshapeResponseToQueryShape ir)
|
||||||
|
where
|
||||||
|
translateQueryDB ::
|
||||||
|
QueryDB 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
m API.QueryRequest
|
||||||
|
translateQueryDB =
|
||||||
|
\case
|
||||||
|
QDBMultipleRows annSelect -> translateAnnSelectToQueryRequest (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) annSelect
|
||||||
|
QDBSingleRow annSelect -> translateAnnSelectToQueryRequest (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) annSelect
|
||||||
|
QDBAggregation annSelect -> translateAnnSelectToQueryRequest (translateTableAggregateFields sessionVariables) annSelect
|
||||||
|
|
||||||
|
translateAnnSelectToQueryRequest ::
|
||||||
|
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT TableRelationships m FieldsAndAggregates) ->
|
||||||
|
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
|
||||||
|
m API.QueryRequest
|
||||||
|
translateAnnSelectToQueryRequest translateFieldsAndAggregates selectG = do
|
||||||
|
tableName <- extractTableName selectG
|
||||||
|
(query, (TableRelationships tableRelationships)) <- CPS.runWriterT (translateAnnSelect sessionVariables translateFieldsAndAggregates tableName selectG)
|
||||||
|
let apiTableRelationships = uncurry API.TableRelationships <$> HashMap.toList tableRelationships
|
||||||
|
pure $
|
||||||
|
API.QueryRequest
|
||||||
|
{ _qrTable = tableName,
|
||||||
|
_qrTableRelationships = apiTableRelationships,
|
||||||
|
_qrQuery = query
|
||||||
|
}
|
||||||
|
|
||||||
|
extractTableName :: MonadError QErr m => AnnSelectG 'DataConnector fieldsType valueType -> m API.TableName
|
||||||
|
extractTableName selectG =
|
||||||
|
case _asnFrom selectG of
|
||||||
|
FromTable tn -> pure $ Witch.from tn
|
||||||
|
FromIdentifier _ -> throw400 NotSupported "AnnSelectG: FromIdentifier not supported"
|
||||||
|
FromFunction {} -> throw400 NotSupported "AnnSelectG: FromFunction not supported"
|
||||||
|
|
||||||
|
translateAnnSelect ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m API.Query
|
||||||
|
translateAnnSelect sessionVariables translateFieldsAndAggregates tableName selectG = do
|
||||||
|
FieldsAndAggregates {..} <- translateFieldsAndAggregates tableName (_asnFields selectG)
|
||||||
|
let whereClauseWithPermissions =
|
||||||
|
case _saWhere (_asnArgs selectG) of
|
||||||
|
Just expr -> BoolAnd [expr, _tpFilter (_asnPerm selectG)]
|
||||||
|
Nothing -> _tpFilter (_asnPerm selectG)
|
||||||
|
whereClause <- translateBoolExpToExpression sessionVariables tableName whereClauseWithPermissions
|
||||||
|
orderBy <- traverse (translateOrderBy sessionVariables tableName) (_saOrderBy $ _asnArgs selectG)
|
||||||
|
pure
|
||||||
|
API.Query
|
||||||
|
{ _qFields = mapFieldNameHashMap _faaFields,
|
||||||
|
_qAggregates = mapFieldNameHashMap _faaAggregates,
|
||||||
|
_qLimit =
|
||||||
|
fmap getMin $
|
||||||
|
foldMap
|
||||||
|
(fmap Min)
|
||||||
|
[ _saLimit (_asnArgs selectG),
|
||||||
|
_tpLimit (_asnPerm selectG)
|
||||||
|
],
|
||||||
|
_qOffset = fmap fromIntegral (_saOffset (_asnArgs selectG)),
|
||||||
|
_qWhere = whereClause,
|
||||||
|
_qOrderBy = orderBy
|
||||||
|
}
|
||||||
|
|
||||||
|
translateOrderBy ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
NE.NonEmpty (AnnotatedOrderByItemG 'DataConnector (UnpreparedValue 'DataConnector)) ->
|
||||||
|
CPS.WriterT writerOutput m API.OrderBy
|
||||||
|
translateOrderBy sessionVariables sourceTableName orderByItems = do
|
||||||
|
orderByElementsAndRelations <- for orderByItems \OrderByItemG {..} -> do
|
||||||
|
let orderDirection = maybe API.Ascending Witch.from obiType
|
||||||
|
translateOrderByElement sessionVariables sourceTableName orderDirection [] obiColumn
|
||||||
|
relations <- lift . mergeOrderByRelations $ snd <$> orderByElementsAndRelations
|
||||||
|
pure
|
||||||
|
API.OrderBy
|
||||||
|
{ _obRelations = relations,
|
||||||
|
_obElements = fst <$> orderByElementsAndRelations
|
||||||
|
}
|
||||||
|
|
||||||
|
translateOrderByElement ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
API.OrderDirection ->
|
||||||
|
[API.RelationshipName] ->
|
||||||
|
AnnotatedOrderByElement 'DataConnector (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m (API.OrderByElement, HashMap API.RelationshipName API.OrderByRelation)
|
||||||
|
translateOrderByElement sessionVariables sourceTableName orderDirection targetReversePath = \case
|
||||||
|
AOCColumn (ColumnInfo {..}) ->
|
||||||
|
pure
|
||||||
|
( API.OrderByElement
|
||||||
|
{ _obeTargetPath = reverse targetReversePath,
|
||||||
|
_obeTarget = API.OrderByColumn $ Witch.from ciColumn,
|
||||||
|
_obeOrderDirection = orderDirection
|
||||||
|
},
|
||||||
|
mempty
|
||||||
|
)
|
||||||
|
AOCObjectRelation relationshipInfo filterExp orderByElement -> do
|
||||||
|
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
||||||
|
(translatedOrderByElement, subOrderByRelations) <- translateOrderByElement sessionVariables _rTargetTable orderDirection (relationshipName : targetReversePath) orderByElement
|
||||||
|
|
||||||
|
targetTableWhereExp <- translateBoolExpToExpression sessionVariables _rTargetTable filterExp
|
||||||
|
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp subOrderByRelations)]
|
||||||
|
|
||||||
|
pure (translatedOrderByElement, orderByRelations)
|
||||||
|
AOCArrayAggregation relationshipInfo filterExp aggregateOrderByElement -> do
|
||||||
|
(relationshipName, API.Relationship {..}) <- recordTableRelationshipFromRelInfo sourceTableName relationshipInfo
|
||||||
|
orderByTarget <- case aggregateOrderByElement of
|
||||||
|
AAOCount ->
|
||||||
|
pure API.OrderByStarCountAggregate
|
||||||
|
AAOOp aggFunctionTxt ColumnInfo {..} -> do
|
||||||
|
aggFunction <- lift $ translateSingleColumnAggregateFunction aggFunctionTxt
|
||||||
|
pure . API.OrderBySingleColumnAggregate $ API.SingleColumnAggregate aggFunction $ Witch.from ciColumn
|
||||||
|
|
||||||
|
let translatedOrderByElement =
|
||||||
|
API.OrderByElement
|
||||||
|
{ _obeTargetPath = reverse (relationshipName : targetReversePath),
|
||||||
|
_obeTarget = orderByTarget,
|
||||||
|
_obeOrderDirection = orderDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTableWhereExp <- translateBoolExpToExpression sessionVariables _rTargetTable filterExp
|
||||||
|
let orderByRelations = HashMap.fromList [(relationshipName, API.OrderByRelation targetTableWhereExp mempty)]
|
||||||
|
pure (translatedOrderByElement, orderByRelations)
|
||||||
|
|
||||||
|
mergeOrderByRelations ::
|
||||||
|
forall m f.
|
||||||
|
(MonadError QErr m, Foldable f) =>
|
||||||
|
f (HashMap API.RelationshipName API.OrderByRelation) ->
|
||||||
|
m (HashMap API.RelationshipName API.OrderByRelation)
|
||||||
|
mergeOrderByRelations orderByRelationsList =
|
||||||
|
foldM mergeMap mempty orderByRelationsList
|
||||||
|
where
|
||||||
|
mergeMap :: HashMap API.RelationshipName API.OrderByRelation -> HashMap API.RelationshipName API.OrderByRelation -> m (HashMap API.RelationshipName API.OrderByRelation)
|
||||||
|
mergeMap left right = foldM (\targetMap (relName, orderByRel) -> HashMap.alterF (maybe (pure $ Just orderByRel) (fmap Just . mergeOrderByRelation orderByRel)) relName targetMap) left $ HashMap.toList right
|
||||||
|
|
||||||
|
mergeOrderByRelation :: API.OrderByRelation -> API.OrderByRelation -> m API.OrderByRelation
|
||||||
|
mergeOrderByRelation right left =
|
||||||
|
if API._obrWhere left == API._obrWhere right
|
||||||
|
then do
|
||||||
|
mergedSubrelations <- mergeMap (API._obrSubrelations left) (API._obrSubrelations right)
|
||||||
|
pure $ API.OrderByRelation (API._obrWhere left) mergedSubrelations
|
||||||
|
else throw500 "mergeOrderByRelations: Differing filter expressions found for the same table"
|
||||||
|
|
||||||
|
translateAnnFieldsWithNoAggregates ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
FieldPrefix ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m FieldsAndAggregates
|
||||||
|
translateAnnFieldsWithNoAggregates sessionVariables fieldNamePrefix sourceTableName fields =
|
||||||
|
(\fields' -> FieldsAndAggregates fields' mempty) <$> translateAnnFields sessionVariables fieldNamePrefix sourceTableName fields
|
||||||
|
|
||||||
|
translateAnnFields ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
FieldPrefix ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m (HashMap FieldName API.Field)
|
||||||
|
translateAnnFields sessionVariables fieldNamePrefix sourceTableName fields = do
|
||||||
|
translatedFields <- traverse (traverse (translateAnnField sessionVariables sourceTableName)) fields
|
||||||
|
pure $ HashMap.fromList (mapMaybe (\(fieldName, field) -> (applyPrefix fieldNamePrefix fieldName,) <$> field) translatedFields)
|
||||||
|
|
||||||
|
translateAnnField ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
AnnFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m (Maybe API.Field)
|
||||||
|
translateAnnField sessionVariables sourceTableName = \case
|
||||||
|
AFColumn colField ->
|
||||||
|
-- TODO: make sure certain fields in colField are not in use, since we don't
|
||||||
|
-- support them
|
||||||
|
pure . Just $ API.ColumnField (Witch.from $ _acfColumn colField) (Witch.from . columnTypeToScalarType $ _acfType colField)
|
||||||
|
AFObjectRelation objRel -> do
|
||||||
|
let targetTable = Witch.from $ _aosTableFrom (_aarAnnSelect objRel)
|
||||||
|
let relationshipName = mkRelationshipName $ _aarRelationshipName objRel
|
||||||
|
fields <- translateAnnFields sessionVariables noPrefix targetTable (_aosFields (_aarAnnSelect objRel))
|
||||||
|
whereClause <- translateBoolExpToExpression sessionVariables targetTable (_aosTableFilter (_aarAnnSelect objRel))
|
||||||
|
|
||||||
|
recordTableRelationship
|
||||||
|
sourceTableName
|
||||||
|
relationshipName
|
||||||
|
API.Relationship
|
||||||
|
{ _rTargetTable = targetTable,
|
||||||
|
_rRelationshipType = API.ObjectRelationship,
|
||||||
|
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping objRel)
|
||||||
|
}
|
||||||
|
|
||||||
|
pure . Just . API.RelField $
|
||||||
|
API.RelationshipField
|
||||||
|
relationshipName
|
||||||
|
( API.Query
|
||||||
|
{ _qFields = mapFieldNameHashMap fields,
|
||||||
|
_qAggregates = mempty,
|
||||||
|
_qWhere = whereClause,
|
||||||
|
_qLimit = Nothing,
|
||||||
|
_qOffset = Nothing,
|
||||||
|
_qOrderBy = Nothing
|
||||||
|
}
|
||||||
|
)
|
||||||
|
AFArrayRelation (ASSimple arrayRelationSelect) -> do
|
||||||
|
Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateAnnFieldsWithNoAggregates sessionVariables noPrefix) arrayRelationSelect
|
||||||
|
AFArrayRelation (ASAggregate arrayRelationSelect) ->
|
||||||
|
Just <$> translateArrayRelationSelect sessionVariables sourceTableName (translateTableAggregateFields sessionVariables) arrayRelationSelect
|
||||||
|
AFExpression _literal ->
|
||||||
|
-- We ignore literal text fields (we don't send them to the data connector agent)
|
||||||
|
-- and add them back to the response JSON when we reshape what the agent returns
|
||||||
|
-- to us
|
||||||
|
pure Nothing
|
||||||
|
|
||||||
|
translateArrayRelationSelect ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
(API.TableName -> Fields (fieldType (UnpreparedValue 'DataConnector)) -> CPS.WriterT writerOutput m FieldsAndAggregates) ->
|
||||||
|
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType (UnpreparedValue 'DataConnector)) ->
|
||||||
|
CPS.WriterT writerOutput m API.Field
|
||||||
|
translateArrayRelationSelect sessionVariables sourceTableName translateFieldsAndAggregates arrRel = do
|
||||||
|
targetTable <- lift $ extractTableName (_aarAnnSelect arrRel)
|
||||||
|
query <- translateAnnSelect sessionVariables translateFieldsAndAggregates targetTable (_aarAnnSelect arrRel)
|
||||||
|
let relationshipName = mkRelationshipName $ _aarRelationshipName arrRel
|
||||||
|
|
||||||
|
recordTableRelationship
|
||||||
|
sourceTableName
|
||||||
|
relationshipName
|
||||||
|
API.Relationship
|
||||||
|
{ _rTargetTable = targetTable,
|
||||||
|
_rRelationshipType = API.ArrayRelationship,
|
||||||
|
_rColumnMapping = HashMap.fromList $ bimap Witch.from Witch.from <$> HashMap.toList (_aarColumnMapping arrRel)
|
||||||
|
}
|
||||||
|
|
||||||
|
pure . API.RelField $
|
||||||
|
API.RelationshipField
|
||||||
|
relationshipName
|
||||||
|
query
|
||||||
|
|
||||||
|
translateTableAggregateFields ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
TableAggregateFieldsG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m FieldsAndAggregates
|
||||||
|
translateTableAggregateFields sessionVariables sourceTableName fields = do
|
||||||
|
mconcat <$> traverse (uncurry (translateTableAggregateField sessionVariables sourceTableName)) fields
|
||||||
|
|
||||||
|
translateTableAggregateField ::
|
||||||
|
( Has TableRelationships writerOutput,
|
||||||
|
Monoid writerOutput,
|
||||||
|
MonadError QErr m
|
||||||
|
) =>
|
||||||
|
SessionVariables ->
|
||||||
|
API.TableName ->
|
||||||
|
FieldName ->
|
||||||
|
TableAggregateFieldG 'DataConnector Void (UnpreparedValue 'DataConnector) ->
|
||||||
|
CPS.WriterT writerOutput m FieldsAndAggregates
|
||||||
|
translateTableAggregateField sessionVariables sourceTableName fieldName = \case
|
||||||
|
TAFAgg aggregateFields -> do
|
||||||
|
let fieldNamePrefix = prefixWith fieldName
|
||||||
|
translatedAggregateFields <- lift $ mconcat <$> traverse (uncurry (translateAggregateField fieldNamePrefix)) aggregateFields
|
||||||
|
pure $
|
||||||
|
FieldsAndAggregates
|
||||||
|
mempty
|
||||||
|
translatedAggregateFields
|
||||||
|
TAFNodes _ fields ->
|
||||||
|
translateAnnFieldsWithNoAggregates sessionVariables (prefixWith fieldName) sourceTableName fields
|
||||||
|
TAFExp _txt ->
|
||||||
|
-- We ignore literal text fields (we don't send them to the data connector agent)
|
||||||
|
-- and add them back to the response JSON when we reshape what the agent returns
|
||||||
|
-- to us
|
||||||
|
pure mempty
|
||||||
|
|
||||||
|
translateAggregateField ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
FieldPrefix ->
|
||||||
|
FieldName ->
|
||||||
|
AggregateField 'DataConnector ->
|
||||||
|
m (HashMap FieldName API.Aggregate)
|
||||||
|
translateAggregateField fieldPrefix fieldName = \case
|
||||||
|
AFCount countAggregate ->
|
||||||
|
let aggregate =
|
||||||
|
case countAggregate of
|
||||||
|
StarCount -> API.StarCount
|
||||||
|
ColumnCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = False}
|
||||||
|
ColumnDistinctCount column -> API.ColumnCount $ API.ColumnCountAggregate {_ccaColumn = Witch.from column, _ccaDistinct = True}
|
||||||
|
in pure $ HashMap.singleton (applyPrefix fieldPrefix fieldName) aggregate
|
||||||
|
AFOp AggregateOp {..} -> do
|
||||||
|
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
|
||||||
|
aggFunction <- translateSingleColumnAggregateFunction _aoOp
|
||||||
|
|
||||||
|
fmap (HashMap.fromList . catMaybes) . forM _aoFields $ \(columnFieldName, columnField) ->
|
||||||
|
case columnField of
|
||||||
|
CFCol column _columnType ->
|
||||||
|
pure . Just $ (applyPrefix fieldPrefix' columnFieldName, API.SingleColumn . API.SingleColumnAggregate aggFunction $ Witch.from column)
|
||||||
|
CFExp _txt ->
|
||||||
|
-- We ignore literal text fields (we don't send them to the data connector agent)
|
||||||
|
-- and add them back to the response JSON when we reshape what the agent returns
|
||||||
|
-- to us
|
||||||
|
pure Nothing
|
||||||
|
AFExp _txt ->
|
||||||
|
-- We ignore literal text fields (we don't send them to the data connector agent)
|
||||||
|
-- and add them back to the response JSON when we reshape what the agent returns
|
||||||
|
-- to us
|
||||||
|
pure mempty
|
||||||
|
|
||||||
|
translateSingleColumnAggregateFunction :: MonadError QErr m => Text -> m API.SingleColumnAggregateFunction
|
||||||
|
translateSingleColumnAggregateFunction functionName =
|
||||||
|
fmap API.SingleColumnAggregateFunction (G.mkName functionName)
|
||||||
|
`onNothing` throw500 ("translateSingleColumnAggregateFunction: Invalid aggregate function encountered: " <> functionName)
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
-- | Validate if a 'API.QueryRequest' contains any relationships.
|
||||||
|
queryHasRelations :: API.QueryRequest -> Bool
|
||||||
|
queryHasRelations API.QueryRequest {..} = _qrTableRelationships /= mempty
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
reshapeResponseToQueryShape ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
QueryDB 'DataConnector Void v ->
|
||||||
|
API.QueryResponse ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeResponseToQueryShape queryDb response =
|
||||||
|
case queryDb of
|
||||||
|
QDBMultipleRows simpleSelect -> reshapeSimpleSelectRows Many (_asnFields simpleSelect) response
|
||||||
|
QDBSingleRow simpleSelect -> reshapeSimpleSelectRows Single (_asnFields simpleSelect) response
|
||||||
|
QDBAggregation aggregateSelect -> reshapeTableAggregateFields (_asnFields aggregateSelect) response
|
||||||
|
|
||||||
|
reshapeSimpleSelectRows ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
Cardinality ->
|
||||||
|
AnnFieldsG 'DataConnector Void v ->
|
||||||
|
API.QueryResponse ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeSimpleSelectRows cardinality fields API.QueryResponse {..} =
|
||||||
|
case cardinality of
|
||||||
|
Single ->
|
||||||
|
case rows of
|
||||||
|
[] -> pure $ J.toEncoding J.Null
|
||||||
|
[singleRow] -> reshapeAnnFields noPrefix fields singleRow
|
||||||
|
_multipleRows ->
|
||||||
|
throw500 "Data Connector agent returned multiple rows when only one was expected" -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
Many -> do
|
||||||
|
reshapedRows <- traverse (reshapeAnnFields noPrefix fields) rows
|
||||||
|
pure $ JE.list id reshapedRows
|
||||||
|
where
|
||||||
|
rows = fromMaybe mempty _qrRows
|
||||||
|
|
||||||
|
reshapeTableAggregateFields ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
TableAggregateFieldsG 'DataConnector Void v ->
|
||||||
|
API.QueryResponse ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeTableAggregateFields tableAggregateFields API.QueryResponse {..} = do
|
||||||
|
reshapedFields <- forM tableAggregateFields $ \(fieldName@(FieldName fieldNameText), tableAggregateField) -> do
|
||||||
|
case tableAggregateField of
|
||||||
|
TAFAgg aggregateFields -> do
|
||||||
|
reshapedAggregateFields <- reshapeAggregateFields (prefixWith fieldName) aggregateFields responseAggregates
|
||||||
|
pure $ (fieldNameText, reshapedAggregateFields)
|
||||||
|
TAFNodes _ annFields -> do
|
||||||
|
reshapedRows <- traverse (reshapeAnnFields (prefixWith fieldName) annFields) responseRows
|
||||||
|
pure $ (fieldNameText, JE.list id reshapedRows)
|
||||||
|
TAFExp txt ->
|
||||||
|
pure $ (fieldNameText, JE.text txt)
|
||||||
|
pure $ encodeAssocListAsObject reshapedFields
|
||||||
|
where
|
||||||
|
responseRows = fromMaybe mempty _qrRows
|
||||||
|
responseAggregates = fromMaybe mempty _qrAggregates
|
||||||
|
|
||||||
|
reshapeAggregateFields ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
FieldPrefix ->
|
||||||
|
AggregateFields 'DataConnector ->
|
||||||
|
HashMap API.FieldName J.Value ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeAggregateFields fieldPrefix aggregateFields responseAggregates = do
|
||||||
|
reshapedFields <- forM aggregateFields $ \(fieldName@(FieldName fieldNameText), aggregateField) ->
|
||||||
|
case aggregateField of
|
||||||
|
AFCount _countAggregate -> do
|
||||||
|
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix fieldName
|
||||||
|
responseAggregateValue <-
|
||||||
|
HashMap.lookup fieldNameKey responseAggregates
|
||||||
|
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName fieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
pure (fieldNameText, J.toEncoding responseAggregateValue)
|
||||||
|
AFOp AggregateOp {..} -> do
|
||||||
|
reshapedColumnFields <- forM _aoFields $ \(columnFieldName@(FieldName columnFieldNameText), columnField) ->
|
||||||
|
case columnField of
|
||||||
|
CFCol _column _columnType -> do
|
||||||
|
let fieldPrefix' = fieldPrefix <> prefixWith fieldName
|
||||||
|
let columnFieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldPrefix' columnFieldName
|
||||||
|
responseAggregateValue <-
|
||||||
|
HashMap.lookup columnFieldNameKey responseAggregates
|
||||||
|
`onNothing` throw500 ("Unable to find expected aggregate " <> API.unFieldName columnFieldNameKey <> " in aggregates returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
pure (columnFieldNameText, J.toEncoding responseAggregateValue)
|
||||||
|
CFExp txt ->
|
||||||
|
pure (columnFieldNameText, JE.text txt)
|
||||||
|
pure (fieldNameText, encodeAssocListAsObject reshapedColumnFields)
|
||||||
|
AFExp txt ->
|
||||||
|
pure (fieldNameText, JE.text txt)
|
||||||
|
pure $ encodeAssocListAsObject reshapedFields
|
||||||
|
|
||||||
|
reshapeAnnFields ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
FieldPrefix ->
|
||||||
|
AnnFieldsG 'DataConnector Void v ->
|
||||||
|
HashMap API.FieldName API.FieldValue ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeAnnFields fieldNamePrefix fields responseRow = do
|
||||||
|
reshapedFields <- forM fields $ \(fieldName@(FieldName fieldNameText), field) -> do
|
||||||
|
let fieldNameKey = API.FieldName . getFieldNameTxt $ applyPrefix fieldNamePrefix fieldName
|
||||||
|
let responseField =
|
||||||
|
HashMap.lookup fieldNameKey responseRow
|
||||||
|
`onNothing` throw500 ("Unable to find expected field " <> API.unFieldName fieldNameKey <> " in row returned by Data Connector agent") -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
reshapedField <- reshapeField field responseField
|
||||||
|
pure (fieldNameText, reshapedField)
|
||||||
|
|
||||||
|
pure $ encodeAssocListAsObject reshapedFields
|
||||||
|
|
||||||
|
reshapeField ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
AnnFieldG 'DataConnector Void v ->
|
||||||
|
m API.FieldValue -> -- This lookup is lazy (behind the monad) so that we can _not_ do it when we've got an AFExpression
|
||||||
|
m J.Encoding
|
||||||
|
reshapeField field responseFieldValue =
|
||||||
|
case field of
|
||||||
|
AFColumn _columnField -> do
|
||||||
|
columnFieldValue <- API.deserializeAsColumnFieldValue <$> responseFieldValue
|
||||||
|
pure $ J.toEncoding columnFieldValue
|
||||||
|
AFObjectRelation objectRelationField -> do
|
||||||
|
relationshipFieldValue <- API.deserializeAsRelationshipFieldValue <$> responseFieldValue
|
||||||
|
case relationshipFieldValue of
|
||||||
|
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
Right subqueryResponse ->
|
||||||
|
let fields = _aosFields $ _aarAnnSelect objectRelationField
|
||||||
|
in reshapeSimpleSelectRows Single fields subqueryResponse
|
||||||
|
AFArrayRelation (ASSimple simpleArrayRelationField) ->
|
||||||
|
reshapeAnnRelationSelect (reshapeSimpleSelectRows Many) simpleArrayRelationField =<< responseFieldValue
|
||||||
|
AFArrayRelation (ASAggregate aggregateArrayRelationField) ->
|
||||||
|
reshapeAnnRelationSelect reshapeTableAggregateFields aggregateArrayRelationField =<< responseFieldValue
|
||||||
|
AFExpression txt -> pure $ JE.text txt
|
||||||
|
|
||||||
|
reshapeAnnRelationSelect ::
|
||||||
|
MonadError QErr m =>
|
||||||
|
(Fields (fieldType v) -> API.QueryResponse -> m J.Encoding) ->
|
||||||
|
AnnRelationSelectG 'DataConnector (AnnSelectG 'DataConnector fieldType v) ->
|
||||||
|
API.FieldValue ->
|
||||||
|
m J.Encoding
|
||||||
|
reshapeAnnRelationSelect reshapeFields annRelationSelect fieldValue =
|
||||||
|
case API.deserializeAsRelationshipFieldValue fieldValue of
|
||||||
|
Left err -> throw500 $ "Found column field value where relationship field value was expected in field returned by Data Connector agent: " <> err -- TODO(dchambers): Add pathing information for error clarity
|
||||||
|
Right subqueryResponse ->
|
||||||
|
let annSimpleSelect = _aarAnnSelect annRelationSelect
|
||||||
|
in reshapeFields (_asnFields annSimpleSelect) subqueryResponse
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
mapFieldNameHashMap :: Eq v => HashMap FieldName v -> Maybe (HashMap API.FieldName v)
|
||||||
|
mapFieldNameHashMap = memptyToNothing . HashMap.mapKeys (API.FieldName . getFieldNameTxt)
|
||||||
|
|
||||||
|
memptyToNothing :: (Monoid m, Eq m) => m -> Maybe m
|
||||||
|
memptyToNothing m = if m == mempty then Nothing else Just m
|
@ -240,12 +240,13 @@ msDBMutationPlan ::
|
|||||||
MonadReader QueryTagsComment m
|
MonadReader QueryTagsComment m
|
||||||
) =>
|
) =>
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
|
Env.Environment ->
|
||||||
Options.StringifyNumbers ->
|
Options.StringifyNumbers ->
|
||||||
SourceName ->
|
SourceName ->
|
||||||
SourceConfig 'MSSQL ->
|
SourceConfig 'MSSQL ->
|
||||||
MutationDB 'MSSQL Void (UnpreparedValue 'MSSQL) ->
|
MutationDB 'MSSQL Void (UnpreparedValue 'MSSQL) ->
|
||||||
m (DBStepInfo 'MSSQL)
|
m (DBStepInfo 'MSSQL)
|
||||||
msDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do
|
msDBMutationPlan userInfo _environment stringifyNum sourceName sourceConfig mrf = do
|
||||||
go <$> case mrf of
|
go <$> case mrf of
|
||||||
MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert
|
MDBInsert annInsert -> executeInsert userInfo stringifyNum sourceConfig annInsert
|
||||||
MDBDelete annDelete -> executeDelete userInfo stringifyNum sourceConfig annDelete
|
MDBDelete annDelete -> executeDelete userInfo stringifyNum sourceConfig annDelete
|
||||||
|
@ -281,12 +281,13 @@ pgDBMutationPlan ::
|
|||||||
MonadReader QueryTagsComment m
|
MonadReader QueryTagsComment m
|
||||||
) =>
|
) =>
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
|
Env.Environment ->
|
||||||
Options.StringifyNumbers ->
|
Options.StringifyNumbers ->
|
||||||
SourceName ->
|
SourceName ->
|
||||||
SourceConfig ('Postgres pgKind) ->
|
SourceConfig ('Postgres pgKind) ->
|
||||||
MutationDB ('Postgres pgKind) Void (UnpreparedValue ('Postgres pgKind)) ->
|
MutationDB ('Postgres pgKind) Void (UnpreparedValue ('Postgres pgKind)) ->
|
||||||
m (DBStepInfo ('Postgres pgKind))
|
m (DBStepInfo ('Postgres pgKind))
|
||||||
pgDBMutationPlan userInfo stringifyNum sourceName sourceConfig mrf = do
|
pgDBMutationPlan userInfo _environment stringifyNum sourceName sourceConfig mrf = do
|
||||||
go <$> case mrf of
|
go <$> case mrf of
|
||||||
MDBInsert s -> convertInsert userInfo s stringifyNum
|
MDBInsert s -> convertInsert userInfo s stringifyNum
|
||||||
MDBUpdate s -> convertUpdate userInfo s stringifyNum
|
MDBUpdate s -> convertUpdate userInfo s stringifyNum
|
||||||
|
@ -83,6 +83,7 @@ class
|
|||||||
MonadReader QueryTagsComment m
|
MonadReader QueryTagsComment m
|
||||||
) =>
|
) =>
|
||||||
UserInfo ->
|
UserInfo ->
|
||||||
|
Env.Environment ->
|
||||||
Options.StringifyNumbers ->
|
Options.StringifyNumbers ->
|
||||||
SourceName ->
|
SourceName ->
|
||||||
SourceConfig b ->
|
SourceConfig b ->
|
||||||
|
@ -129,7 +129,7 @@ convertMutationSelectionSet
|
|||||||
let mutationQueryTagsAttributes = encodeQueryTags $ QTMutation $ MutationMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
let mutationQueryTagsAttributes = encodeQueryTags $ QTMutation $ MutationMetadata reqId maybeOperationName rootFieldName parameterizedQueryHash
|
||||||
queryTagsComment = Tagged.untag $ createQueryTags @m mutationQueryTagsAttributes queryTagsConfig
|
queryTagsComment = Tagged.untag $ createQueryTags @m mutationQueryTagsAttributes queryTagsConfig
|
||||||
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsMutationDB db
|
(noRelsDBAST, remoteJoins) = RJ.getRemoteJoinsMutationDB db
|
||||||
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBMutationPlan @b userInfo stringifyNum sourceName sourceConfig noRelsDBAST
|
dbStepInfo <- flip runReaderT queryTagsComment $ mkDBMutationPlan @b userInfo env stringifyNum sourceName sourceConfig noRelsDBAST
|
||||||
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
pure $ ExecStepDB [] (AB.mkAnyBackend dbStepInfo) remoteJoins
|
||||||
RFRemote remoteField -> do
|
RFRemote remoteField -> do
|
||||||
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
RemoteSchemaRootField remoteSchemaInfo resultCustomizer resolvedRemoteField <- runVariableCache $ resolveRemoteField userInfo remoteField
|
||||||
|
@ -73,11 +73,12 @@ spec = do
|
|||||||
let returningFields = [(FieldName "field", ColumnField (ColumnName "my_column") (ScalarType "string"))]
|
let returningFields = [(FieldName "field", ColumnField (ColumnName "my_column") (ScalarType "string"))]
|
||||||
describe "InsertOperation" $ do
|
describe "InsertOperation" $ do
|
||||||
testToFromJSONToSchema
|
testToFromJSONToSchema
|
||||||
(InsertOperation (InsertMutationOperation (TableName ["my_table"]) [] returningFields))
|
(InsertOperation (InsertMutationOperation (TableName ["my_table"]) [] (Just $ And []) returningFields))
|
||||||
[aesonQQ|
|
[aesonQQ|
|
||||||
{ "type": "insert",
|
{ "type": "insert",
|
||||||
"table": ["my_table"],
|
"table": ["my_table"],
|
||||||
"rows": [],
|
"rows": [],
|
||||||
|
"post_insert_check": { "type": "and", "expressions": [] },
|
||||||
"returning_fields": {
|
"returning_fields": {
|
||||||
"field": {
|
"field": {
|
||||||
"type": "column",
|
"type": "column",
|
||||||
@ -89,12 +90,13 @@ spec = do
|
|||||||
|]
|
|]
|
||||||
describe "UpdateOperation" $ do
|
describe "UpdateOperation" $ do
|
||||||
testToFromJSONToSchema
|
testToFromJSONToSchema
|
||||||
(UpdateOperation (UpdateMutationOperation (TableName ["my_table"]) (Just $ And []) [] returningFields))
|
(UpdateOperation (UpdateMutationOperation (TableName ["my_table"]) (Just $ And []) [] (Just $ And []) returningFields))
|
||||||
[aesonQQ|
|
[aesonQQ|
|
||||||
{ "type": "update",
|
{ "type": "update",
|
||||||
"table": ["my_table"],
|
"table": ["my_table"],
|
||||||
"where": { "type": "and", "expressions": [] },
|
"where": { "type": "and", "expressions": [] },
|
||||||
"updates": [],
|
"updates": [],
|
||||||
|
"post_update_check": { "type": "and", "expressions": [] },
|
||||||
"returning_fields": {
|
"returning_fields": {
|
||||||
"field": {
|
"field": {
|
||||||
"type": "column",
|
"type": "column",
|
||||||
@ -272,6 +274,7 @@ genInsertMutationOperation =
|
|||||||
InsertMutationOperation
|
InsertMutationOperation
|
||||||
<$> genTableName
|
<$> genTableName
|
||||||
<*> Gen.list defaultRange genRowObject
|
<*> Gen.list defaultRange genRowObject
|
||||||
|
<*> Gen.maybe genExpression
|
||||||
<*> genFieldMap genField
|
<*> genFieldMap genField
|
||||||
|
|
||||||
genRowObject :: Gen RowObject
|
genRowObject :: Gen RowObject
|
||||||
@ -292,6 +295,7 @@ genUpdateMutationOperation =
|
|||||||
<$> genTableName
|
<$> genTableName
|
||||||
<*> Gen.maybe genExpression
|
<*> Gen.maybe genExpression
|
||||||
<*> Gen.list defaultRange genRowUpdate
|
<*> Gen.list defaultRange genRowUpdate
|
||||||
|
<*> Gen.maybe genExpression
|
||||||
<*> genFieldMap genField
|
<*> genFieldMap genField
|
||||||
|
|
||||||
genRowUpdate :: (MonadGen m, GenBase m ~ Identity) => m RowUpdate
|
genRowUpdate :: (MonadGen m, GenBase m ~ Identity) => m RowUpdate
|
||||||
|
Loading…
Reference in New Issue
Block a user