diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e9bba5c8b..2bbcc42b276 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - server: update `create_scheduled_event` API to return `event_id` in response - server: fix bug which allowed inconsistent metadata to exist after the `replace_metadata` API even though `allow_inconsistent_object` is set to `false`. - server: fix explicit `null` values not allowed in nested object relationship inserts (#7484) +- server: prevent empty subscription roots in the schema (#6898) - console: support tracking of functions with return a single row ## v2.0.9 diff --git a/cli/internal/hasura/v1graphql/testdata/latest.golden b/cli/internal/hasura/v1graphql/testdata/latest.golden index 54f787ed68d..656fae7e257 100644 --- a/cli/internal/hasura/v1graphql/testdata/latest.golden +++ b/cli/internal/hasura/v1graphql/testdata/latest.golden @@ -92,9 +92,7 @@ "queryType": { "name": "query_root" }, - "subscriptionType": { - "name": "subscription_root" - }, + "subscriptionType": null, "types": [ { "inputFields": null, @@ -766,16 +764,6 @@ "enumValues": null, "description": null, "fields": [] - }, - { - "inputFields": null, - "kind": "OBJECT", - "possibleTypes": null, - "interfaces": [], - "name": "subscription_root", - "enumValues": null, - "description": null, - "fields": [] } ], "mutationType": null diff --git a/server/src-lib/Hasura/GraphQL/Schema.hs b/server/src-lib/Hasura/GraphQL/Schema.hs index e8021e9972f..493453f648f 100644 --- a/server/src-lib/Hasura/GraphQL/Schema.hs +++ b/server/src-lib/Hasura/GraphQL/Schema.hs @@ -256,7 +256,7 @@ buildRelayRoleContext mutationParserBackend <- buildMutationParser mempty allActionInfos nonObjectCustomTypes mutationBackendFields subscriptionParser <- - P.safeSelectionSet subscriptionRoot Nothing queryPGFields <&> fmap (fmap typenameToRawRF) + buildSubscriptionParser queryPGFields [] queryParserFrontend <- queryWithIntrospectionHelper queryPGFields mutationParserFrontend subscriptionParser queryParserBackend <- @@ -341,16 +341,14 @@ unauthenticatedContext -> RemoteSchemaPermsCtx -> m GQLContext unauthenticatedContext adminQueryRemotes adminMutationRemotes remoteSchemaPermsCtx = P.runSchemaT $ do - let isRemoteSchemaPermsEnabled = remoteSchemaPermsCtx == RemoteSchemaPermsEnabled - queryFields = bool (fmap (fmap RFRemote) adminQueryRemotes) [] isRemoteSchemaPermsEnabled - mutationFields = bool (fmap (fmap RFRemote) adminMutationRemotes) [] isRemoteSchemaPermsEnabled - mutationParser <- - if null adminMutationRemotes - then pure Nothing - else P.safeSelectionSet mutationRoot Nothing mutationFields <&> Just . fmap (fmap typenameToRawRF) - subscriptionParser <- - P.safeSelectionSet subscriptionRoot Nothing [] <&> fmap (fmap typenameToRawRF) - queryParser <- queryWithIntrospectionHelper queryFields mutationParser subscriptionParser + let + isRemoteSchemaPermsEnabled = remoteSchemaPermsCtx == RemoteSchemaPermsEnabled + queryFields = bool (fmap (fmap RFRemote) adminQueryRemotes) [] isRemoteSchemaPermsEnabled + mutationFields = bool (fmap (fmap RFRemote) adminMutationRemotes) [] isRemoteSchemaPermsEnabled + mutationParser <- whenMaybe (not $ null mutationFields) $ + P.safeSelectionSet mutationRoot (Just $ G.Description "mutation root") mutationFields + <&> fmap (fmap typenameToRawRF) + queryParser <- queryWithIntrospectionHelper queryFields mutationParser Nothing pure $ GQLContext (finalizeParser queryParser) (finalizeParser <$> mutationParser) @@ -543,7 +541,7 @@ buildQueryParser -> [ActionInfo] -> NonObjectTypeMap -> Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (MutationRootField UnpreparedValue))) - -> Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue)) + -> Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue))) -> m (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue))) buildQueryParser pgQueryFields remoteFields allActions nonObjectCustomTypes mutationParser subscriptionParser = do actionQueryFields <- concat <$> traverse (buildActionQueryFields nonObjectCustomTypes) allActions @@ -554,18 +552,18 @@ queryWithIntrospectionHelper :: forall n m. (MonadSchema n m, MonadError QErr m) => [P.FieldParser n (QueryRootField UnpreparedValue)] -> Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (MutationRootField UnpreparedValue))) - -> Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue)) + -> Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue))) -> m (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue))) queryWithIntrospectionHelper basicQueryFP mutationP subscriptionP = do basicQueryP <- queryRootFromFields basicQueryFP emptyIntro <- emptyIntrospection let directives = directivesInfo @n - allBasicTypes <- collectTypes $ - [ P.TypeDefinitionsWrapper $ P.parserType basicQueryP - , P.TypeDefinitionsWrapper $ P.parserType subscriptionP - , P.TypeDefinitionsWrapper $ P.diArguments =<< directives + allBasicTypes <- collectTypes $ catMaybes + [ Just $ P.TypeDefinitionsWrapper $ P.parserType basicQueryP + , Just $ P.TypeDefinitionsWrapper $ P.diArguments =<< directives + , P.TypeDefinitionsWrapper . P.parserType <$> mutationP + , P.TypeDefinitionsWrapper . P.parserType <$> subscriptionP ] - ++ maybeToList (P.TypeDefinitionsWrapper . P.parserType <$> mutationP) allIntrospectionTypes <- collectTypes . P.parserType =<< queryRootFromFields emptyIntro let allTypes = Map.unions [ allBasicTypes @@ -576,7 +574,7 @@ queryWithIntrospectionHelper basicQueryFP mutationP subscriptionP = do , sTypes = allTypes , sQueryType = P.parserType basicQueryP , sMutationType = P.parserType <$> mutationP - , sSubscriptionType = Just $ P.parserType subscriptionP + , sSubscriptionType = P.parserType <$> subscriptionP , sDirectives = directives } let partialQueryFields = @@ -630,11 +628,13 @@ buildSubscriptionParser ) => [P.FieldParser n (QueryRootField UnpreparedValue)] -> [ActionInfo] - -> m (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue))) + -> m (Maybe (Parser 'Output n (OMap.InsOrdHashMap G.Name (QueryRootField UnpreparedValue)))) buildSubscriptionParser queryFields allActions = do actionSubscriptionFields <- concat <$> traverse buildActionSubscriptionFields allActions let subscriptionFields = queryFields <> actionSubscriptionFields - P.safeSelectionSet subscriptionRoot Nothing subscriptionFields <&> fmap (fmap typenameToRawRF) + whenMaybe (not $ null subscriptionFields) $ + P.safeSelectionSet subscriptionRoot Nothing subscriptionFields + <&> fmap (fmap typenameToRawRF) buildMutationParser :: forall m n r @@ -654,10 +654,9 @@ buildMutationParser allRemotes allActions nonObjectCustomTypes mutationFields = mutationFields <> actionParsers <> fmap (fmap RFRemote) allRemotes - if null mutationFieldsParser - then pure Nothing - else P.safeSelectionSet mutationRoot (Just $ G.Description "mutation root") mutationFieldsParser - <&> Just . fmap (fmap typenameToRawRF) + whenMaybe (not $ null mutationFieldsParser) $ + P.safeSelectionSet mutationRoot (Just $ G.Description "mutation root") mutationFieldsParser + <&> fmap (fmap typenameToRawRF) diff --git a/server/tests-py/queries/graphql_query/empty/check_no_empty_roots.yaml b/server/tests-py/queries/graphql_query/empty/check_no_empty_roots.yaml new file mode 100644 index 00000000000..e7b3e8a75d3 --- /dev/null +++ b/server/tests-py/queries/graphql_query/empty/check_no_empty_roots.yaml @@ -0,0 +1,31 @@ +- description: Empty schema does not have a mutation root object + url: /v1/graphql + status: 200 + response: + data: + __type: null + query: + query: | + query { + __type(name: "mutation_root") { + fields { + name + } + } + } + +- description: Empty schema does not have a subscription root object + url: /v1/graphql + status: 200 + response: + data: + __type: null + query: + query: | + query { + __type(name: "subscription_root") { + fields { + name + } + } + } diff --git a/server/tests-py/test_graphql_queries.py b/server/tests-py/test_graphql_queries.py index 381b02e77d8..38a89bad261 100644 --- a/server/tests-py/test_graphql_queries.py +++ b/server/tests-py/test_graphql_queries.py @@ -45,6 +45,17 @@ class TestGraphQLQueryBasicMySQL: return 'queries/graphql_query/mysql' +@pytest.mark.parametrize("transport", ['http', 'websocket']) +class TestGraphQLEmpty: + + def test_no_empty_roots(self, hge_ctx, transport): + check_query_f(hge_ctx, self.dir() + '/check_no_empty_roots.yaml', transport) + + @classmethod + def dir(cls): + return 'queries/graphql_query/empty' + + @pytest.mark.parametrize("transport", ['http', 'websocket']) @pytest.mark.parametrize("backend", ['bigquery']) @usefixtures('per_class_tests_db_state')