diff --git a/server/src-lib/Hasura/GraphQL/Resolve.hs b/server/src-lib/Hasura/GraphQL/Resolve.hs index a67a6e901c3..142a74a5666 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve.hs @@ -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 diff --git a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs index 7774e644c87..3cd112b498b 100644 --- a/server/src-lib/Hasura/GraphQL/Resolve/Action.hs +++ b/server/src-lib/Hasura/GraphQL/Resolve/Action.hs @@ -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 diff --git a/server/tests-py/queries/actions/sync/remote_joins/action_with_remote_joins.yaml b/server/tests-py/queries/actions/sync/remote_joins/action_with_remote_joins.yaml new file mode 100644 index 00000000000..c6fef942ef3 --- /dev/null +++ b/server/tests-py/queries/actions/sync/remote_joins/action_with_remote_joins.yaml @@ -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 diff --git a/server/tests-py/queries/actions/sync/remote_joins/schema_setup.yaml b/server/tests-py/queries/actions/sync/remote_joins/schema_setup.yaml new file mode 100644 index 00000000000..d8b2011ea69 --- /dev/null +++ b/server/tests-py/queries/actions/sync/remote_joins/schema_setup.yaml @@ -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 diff --git a/server/tests-py/queries/actions/sync/remote_joins/schema_teardown.yaml b/server/tests-py/queries/actions/sync/remote_joins/schema_teardown.yaml new file mode 100644 index 00000000000..dee1bee502d --- /dev/null +++ b/server/tests-py/queries/actions/sync/remote_joins/schema_teardown.yaml @@ -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 diff --git a/server/tests-py/queries/actions/sync/remote_joins/values_teardown.yaml b/server/tests-py/queries/actions/sync/remote_joins/values_teardown.yaml new file mode 100644 index 00000000000..f5acee6da0d --- /dev/null +++ b/server/tests-py/queries/actions/sync/remote_joins/values_teardown.yaml @@ -0,0 +1,7 @@ +type: bulk +args: +- type: run_sql + args: + sql: | + DELETE FROM "user"; + SELECT setval('user_id_seq', 1, FALSE); diff --git a/server/tests-py/remote_schemas/nodejs/actions_remote_join_schema.js b/server/tests-py/remote_schemas/nodejs/actions_remote_join_schema.js new file mode 100644 index 00000000000..6ae5a85e325 --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/actions_remote_join_schema.js @@ -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}`); +}); diff --git a/server/tests-py/remote_server.py b/server/tests-py/remote_server.py new file mode 100644 index 00000000000..72ff4b202d6 --- /dev/null +++ b/server/tests-py/remote_server.py @@ -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() diff --git a/server/tests-py/test_actions.py b/server/tests-py/test_actions.py index dde2fd7ae22..c2d2e96e0d1 100644 --- a/server/tests-py/test_actions.py +++ b/server/tests-py/test_actions.py @@ -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) diff --git a/server/tests-py/test_remote_relationships.py b/server/tests-py/test_remote_relationships.py index 86a79f7679b..471724f920a 100644 --- a/server/tests-py/test_remote_relationships.py +++ b/server/tests-py/test_remote_relationships.py @@ -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():