actions: fix behaviour when using remote relationship in mutation action's relationship (#4982)

* resolve the remote server when a remote join query used in action mutation
This commit is contained in:
Karthikeyan Chinnakonda 2020-06-05 17:33:18 +05:30 committed by GitHub
parent e4e4856085
commit d6de3592ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 291 additions and 28 deletions

View File

@ -181,7 +181,7 @@ mutFldToTx fld = do
validateHdrs userInfo (_docHeaders ctx)
noRespHeaders $ RM.convertDeleteByPk ctx rjCtx fld
MCAction ctx ->
RA.resolveActionMutation fld ctx (_uiSession userInfo)
RA.resolveActionMutation fld ctx userInfo
getOpCtx
:: ( MonadReusability m

View File

@ -34,9 +34,9 @@ import qualified Language.GraphQL.Draft.Syntax as G
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types as HTTP
import qualified Network.Wreq as Wreq
import qualified Hasura.GraphQL.Resolve.Select as GRS
import qualified Hasura.RQL.DML.Select as RS
import qualified Hasura.RQL.DML.RemoteJoin as RJ
import Hasura.EncJSON
import Hasura.GraphQL.Resolve.Context
@ -128,14 +128,14 @@ resolveActionMutation
)
=> Field
-> ActionMutationExecutionContext
-> SessionVariables
-> UserInfo
-> m (RespTx, HTTP.ResponseHeaders)
resolveActionMutation field executionContext sessionVariables =
resolveActionMutation field executionContext userInfo =
case executionContext of
ActionMutationSyncWebhook executionContextSync ->
resolveActionMutationSync field executionContextSync sessionVariables
resolveActionMutationSync field executionContextSync userInfo
ActionMutationAsync ->
(,[]) <$> resolveActionMutationAsync field sessionVariables
(,[]) <$> resolveActionMutationAsync field userInfo
-- | Synchronously execute webhook handler and resolve response to action "output"
resolveActionMutationSync
@ -152,11 +152,12 @@ resolveActionMutationSync
)
=> Field
-> ActionExecutionContext
-> SessionVariables
-> UserInfo
-> m (RespTx, HTTP.ResponseHeaders)
resolveActionMutationSync field executionContext sessionVariables = do
resolveActionMutationSync field executionContext userInfo = do
let inputArgs = J.toJSON $ fmap annInpValueToJson $ _fArguments field
actionContext = ActionContext actionName
sessionVariables = _uiSession userInfo
handlerPayload = ActionWebhookPayload actionContext sessionVariables inputArgs
manager <- asks getter
reqHeaders <- asks getter
@ -168,8 +169,16 @@ resolveActionMutationSync field executionContext sessionVariables = do
processOutputSelectionSet webhookResponseExpression outputType definitionList
(_fType field) $ _fSelSet field
astResolved <- RS.traverseAnnSimpleSel resolveValTxt selectAstUnresolved
let (astResolvedWithoutRemoteJoins,maybeRemoteJoins) = RJ.getRemoteJoins astResolved
let jsonAggType = mkJsonAggSelect outputType
return $ (,respHeaders) $ asSingleRowJsonResp (Q.fromBuilder $ toSQL $ RS.mkSQLSelect jsonAggType astResolved) []
return $ (,respHeaders) $
case maybeRemoteJoins of
Just remoteJoins ->
let query = Q.fromBuilder $ toSQL $ RS.mkSQLSelect jsonAggType astResolvedWithoutRemoteJoins
in RJ.executeQueryWithRemoteJoins manager reqHeaders userInfo query [] remoteJoins
Nothing ->
asSingleRowJsonResp (Q.fromBuilder $ toSQL $ RS.mkSQLSelect jsonAggType astResolved) []
where
ActionExecutionContext actionName outputType outputFields definitionList resolvedWebhook confHeaders
forwardClientHeaders = executionContext
@ -243,9 +252,10 @@ resolveActionMutationAsync
, Has [HTTP.Header] r
)
=> Field
-> SessionVariables
-> UserInfo
-> m RespTx
resolveActionMutationAsync field sessionVariables = do
resolveActionMutationAsync field userInfo = do
let sessionVariables = _uiSession userInfo
reqHeaders <- asks getter
let inputArgs = J.toJSON $ fmap annInpValueToJson $ _fArguments field
pure $ do

View File

@ -0,0 +1,49 @@
- description: Create a new remote relationship
url: /v1/query
status: 200
response:
message: success
query:
type: create_remote_relationship
args:
name: messageBasic
table: user
hasura_fields:
- id
remote_schema: actions-remote-join-schema
remote_field:
message:
arguments:
id: "$id"
- description: Run create_users action
url: /v1/graphql
status: 200
query:
query: |
mutation {
create_user(email: "clarke@gmail.com", name: "Clarke"){
id
user {
name
email
is_admin
messageBasic {
name
msg
}
}
}
}
response:
data:
create_user:
id: 1
user:
name: Clarke
email: clarke@gmail.com
is_admin: false
messageBasic:
name: Clarke
msg: Welcome to the team, Clarke

View File

@ -0,0 +1,67 @@
type: bulk
args:
- type: run_sql
args:
sql: |
CREATE TABLE "user"(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT false
);
- type: track_table
args:
name: user
schema: public
- type: set_custom_types
args:
input_objects:
- name: UserInput
fields:
- name: name
type: String!
- name: email
type: String!
- name: InObject
fields:
- name: id
type: ID
- name: name
type: String
- name: age
type: Int
objects:
- name: UserId
fields:
- name: id
type: Int!
relationships:
- name: user
type: object
remote_table: user
field_mapping:
id: id
- type: create_action
args:
name: create_user
definition:
kind: synchronous
arguments:
- name: email
type: String!
- name: name
type: String!
output_type: UserId
handler: http://127.0.0.1:5593/create-user
- type: add_remote_schema
args:
name: actions-remote-join-schema
definition:
url: http://localhost:4001
forward_client_headers: false

View File

@ -0,0 +1,20 @@
type: bulk
args:
- type: drop_action
args:
name: create_user
clear_data: true
- type: set_custom_types
args: {}
- type: run_sql
args:
cascade: true
sql: |
DROP TABLE "user";
# also drops remote relationship as direct dep
- type: remove_remote_schema
args:
name: actions-remote-join-schema

View File

@ -0,0 +1,7 @@
type: bulk
args:
- type: run_sql
args:
sql: |
DELETE FROM "user";
SELECT setval('user_id_seq', 1, FALSE);

View File

@ -0,0 +1,79 @@
const { ApolloServer, ApolloError } = require('apollo-server');
const gql = require('graphql-tag');
const { print } = require('graphql');
const allMessages = [
{ id: 1, name: "Clarke", msg: "Welcome to the team, Clarke"},
{ id: 2, name: "Alice", msg: "Welcome to the team, Alice"},
];
const typeDefs = gql`
interface Communication {
id: Int!
msg: String!
}
type Message implements Communication {
id: Int!
name: String!
msg: String!
errorMsg: String
}
type Query {
hello: String
message(id: Int!) : Message
}
`;
const resolvers = {
Message: {
errorMsg : () => {
throw new ApolloError("intentional-error", "you asked for it");
}
},
Query: {
hello: () => "world",
message: (_, { id }) => {
return allMessages.find(m => m.id == id);
}
},
Communication: {
__resolveType(communication, context, info){
if(communication.name) {
return "Message";
}
return null;
}
}
};
class BasicLogging {
requestDidStart({queryString, parsedQuery, variables}) {
const query = queryString || print(parsedQuery);
console.log(query);
console.log(variables);
}
willSendResponse({graphqlResponse}) {
console.log(JSON.stringify(graphqlResponse, null, 2));
}
}
const schema = new ApolloServer(
{ typeDefs,
resolvers,
extensions: [() => new BasicLogging()],
formatError: (err) => {
// Stack traces make expected test output brittle and noisey:
delete err.extensions;
return err;
} });
schema.listen({ port: process.env.PORT || 4001 }).then(({ url }) => {
console.log(`schema ready at ${url}`);
});

View File

@ -0,0 +1,20 @@
#!/usr/bin/env python3
import subprocess
import time
class NodeGraphQL():
def __init__(self, cmd):
self.cmd = cmd
self.proc = None
def start(self):
proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
time.sleep(1)
proc.poll()
if proc.returncode is not None:
raise Exception("It seems our node graphql test server stopped unexpectedly:\n" + proc.stdout.read().decode('utf-8'))
self.proc = proc
def stop(self):
self.proc.terminate()

View File

@ -2,19 +2,36 @@
import pytest
import time
import subprocess
from validate import check_query_f, check_query, get_conf_f
from remote_server import NodeGraphQL
"""
TODO:- Test Actions metadata
"""
@pytest.fixture(scope="module")
def graphql_service():
svc = NodeGraphQL(["node", "remote_schemas/nodejs/actions_remote_join_schema.js"])
svc.start()
yield svc
svc.stop()
use_action_fixtures = pytest.mark.usefixtures(
"actions_fixture",
'per_class_db_schema_for_mutation_tests',
'per_method_db_data_for_mutation_tests'
)
use_action_fixtures_with_remote_joins = pytest.mark.usefixtures(
"graphql_service",
"actions_fixture",
"per_class_db_schema_for_mutation_tests",
"per_method_db_data_for_mutation_tests"
)
@pytest.mark.parametrize("transport", ['http', 'websocket'])
@use_action_fixtures
class TestActionsSyncWebsocket:
@ -64,6 +81,16 @@ class TestActionsSync:
def test_mirror_action_success(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + '/mirror_action_success.yaml')
@use_action_fixtures_with_remote_joins
class TestActionsSyncWithRemoteJoins:
@classmethod
def dir(cls):
return 'queries/actions/sync/remote_joins'
def test_action_with_remote_joins(self,hge_ctx):
check_query_f(hge_ctx,self.dir() + '/action_with_remote_joins.yaml')
# Check query with admin secret tokens
def check_query_secret(hge_ctx, f):
conf = get_conf_f(f)

View File

@ -5,23 +5,7 @@ import subprocess
import time
from validate import check_query_f, check_query
class NodeGraphQL():
def __init__(self, cmd):
self.cmd = cmd
self.proc = None
def start(self):
proc = subprocess.Popen(self.cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
time.sleep(1)
proc.poll()
if proc.returncode is not None:
raise Exception("It seems our node graphql test server stopped unexpectedly:\n" + proc.stdout.read().decode('utf-8'))
self.proc = proc
def stop(self):
self.proc.terminate()
from remote_server import NodeGraphQL
@pytest.fixture(scope="module")
def graphql_service():