diff --git a/.circleci/test-server.sh b/.circleci/test-server.sh index 0f86e219a0a8d..8e0b3c4d80dc6 100755 --- a/.circleci/test-server.sh +++ b/.circleci/test-server.sh @@ -703,6 +703,32 @@ remote-schema-permissions) kill_hge_servers ;; +remote-schema-prioritize-data) + echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH REMOTE SCHEMA PRIORITIZE DATA/ERRORS ########>\n" + export HASURA_GRAPHQL_ADMIN_SECRET="HGE$RANDOM$RANDOM" + + run_hge_with_args serve + wait_for_port 8080 + + pytest "${PYTEST_COMMON_ARGS[@]}" \ + test_remote_schema_prioritize_none.py + + kill_hge_servers + + export HASURA_GRAPHQL_REMOTE_SCHEMA_PRIORITIZE_DATA=true + + run_hge_with_args serve + wait_for_port 8080 + + pytest "${PYTEST_COMMON_ARGS[@]}" \ + test_remote_schema_prioritize_data.py + + unset HASURA_GRAPHQL_REMOTE_SCHEMA_PRIORITIZE_DATA + + kill_hge_servers + + ;; + function-permissions) echo -e "\n$(time_elapsed): <########## TEST GRAPHQL-ENGINE WITH FUNCTION PERMISSIONS ENABLED ########>\n" export HASURA_GRAPHQL_INFER_FUNCTION_PERMISSIONS=false diff --git a/docs/docs/deployment/graphql-engine-flags/reference.mdx b/docs/docs/deployment/graphql-engine-flags/reference.mdx index ef7e20cc400dc..21cddd75ecf41 100644 --- a/docs/docs/deployment/graphql-engine-flags/reference.mdx +++ b/docs/docs/deployment/graphql-engine-flags/reference.mdx @@ -978,6 +978,19 @@ The Redis URL to use for [query caching](/caching/enterprise-caching.mdx) and | **Example** | `redis://username:password@host:port/db` | | **Supported in** | Enterprise Edition only | +### Remote Schema prioritize data + +Setting this will prioritize `data` or `errors` if both fields are present in the Remote Schema response. + +| | | +| ------------------- | ---------------------------------------------- | +| **Flag** | `--remote-schema-prioritize-data` | +| **Env var** | `HASURA_GRAPHQL_REMOTE_SCHEMA_PRIORITIZE_DATA` | +| **Accepted values** | Boolean | +| **Options** | `true` or `false` | +| **Default** | `false` | +| **Supported in** | CE, Enterprise Edition, Cloud | + ### Schema Sync Poll Interval The interval, in milliseconds, to poll Metadata storage for updates. To disable, set this value to `0`. diff --git a/server/lib/test-harness/src/Harness/Constants.hs b/server/lib/test-harness/src/Harness/Constants.hs index c6d37bf5a0380..0a2687da923b4 100644 --- a/server/lib/test-harness/src/Harness/Constants.hs +++ b/server/lib/test-harness/src/Harness/Constants.hs @@ -316,7 +316,8 @@ serveOptions = soTriggersErrorLogLevelStatus = Init._default Init.triggersErrorLogLevelStatusOption, soAsyncActionsFetchBatchSize = Init._default Init.asyncActionsFetchBatchSizeOption, soPersistedQueries = Init._default Init.persistedQueriesOption, - soPersistedQueriesTtl = Init._default Init.persistedQueriesTtlOption + soPersistedQueriesTtl = Init._default Init.persistedQueriesTtlOption, + soRemoteSchemaResponsePriority = Init._default Init.remoteSchemaResponsePriorityOption } -- | What log level should be used by the engine; this is not exported, and diff --git a/server/src-lib/Hasura/App/State.hs b/server/src-lib/Hasura/App/State.hs index 57c87bfb7df41..3eaf09b48f0ae 100644 --- a/server/src-lib/Hasura/App/State.hs +++ b/server/src-lib/Hasura/App/State.hs @@ -171,7 +171,8 @@ data AppContext = AppContext acAsyncActionsFetchInterval :: OptionalInterval, acApolloFederationStatus :: ApolloFederationStatus, acCloseWebsocketsOnMetadataChangeStatus :: CloseWebsocketsOnMetadataChangeStatus, - acSchemaSampledFeatureFlags :: SchemaSampledFeatureFlags + acSchemaSampledFeatureFlags :: SchemaSampledFeatureFlags, + acRemoteSchemaResponsePriority :: RemoteSchemaResponsePriority } -- | Collection of the LoggerCtx, the regular Logger and the PGLogger @@ -292,7 +293,8 @@ buildAppContextRule = proc (ServeOptions {..}, env, _keys, checkFeatureFlag) -> acAsyncActionsFetchInterval = soAsyncActionsFetchInterval, acApolloFederationStatus = soApolloFederationStatus, acCloseWebsocketsOnMetadataChangeStatus = soCloseWebsocketsOnMetadataChangeStatus, - acSchemaSampledFeatureFlags = schemaSampledFeatureFlags + acSchemaSampledFeatureFlags = schemaSampledFeatureFlags, + acRemoteSchemaResponsePriority = soRemoteSchemaResponsePriority } where buildEventEngineCtx = Inc.cache proc (httpPoolSize, fetchInterval, fetchBatchSize) -> do diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index 650b1fe12c640..fdbc3bd3b132a 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -36,9 +36,11 @@ import Data.ByteString.Lazy qualified as LBS import Data.Dependent.Map qualified as DM import Data.Environment qualified as Env import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap +import Data.List.NonEmpty qualified as NE import Data.Monoid (Any (..)) import Data.Text qualified as T import Data.Text.Extended (toTxt, (<>>)) +import Data.Vector qualified as Vec import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.Postgres.Instances.Transport (runPGMutationTransaction) import Hasura.Base.Error @@ -89,7 +91,7 @@ import Hasura.Server.Prometheus PrometheusMetrics (..), ) import Hasura.Server.Telemetry.Counters qualified as Telem -import Hasura.Server.Types (ModelInfoLogState (..), MonadGetPolicies (..), ReadOnlyMode (..), RequestId (..)) +import Hasura.Server.Types (ModelInfoLogState (..), MonadGetPolicies (..), ReadOnlyMode (..), RemoteSchemaResponsePriority (..), RequestId (..)) import Hasura.Services import Hasura.Session (SessionVariable, SessionVariableValue, SessionVariables, UserInfo (..), filterSessionVariables) import Hasura.Tracing (MonadTrace, attachMetadata) @@ -309,6 +311,7 @@ runGQ :: SchemaCache -> Init.AllowListStatus -> ReadOnlyMode -> + RemoteSchemaResponsePriority -> PrometheusMetrics -> L.Logger L.Hasura -> Maybe (CredentialCache AgentLicenseKey) -> @@ -320,7 +323,7 @@ runGQ :: GQLReqUnparsed -> ResponseInternalErrorsConfig -> m (GQLQueryOperationSuccessLog, HttpResponse (Maybe GQResponse, EncJSON)) -runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHeaders queryType reqUnparsed responseErrorsConfig = do +runGQ env sqlGenCtx sc enableAL readOnlyMode remoteSchemaResponsePriority prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHeaders queryType reqUnparsed responseErrorsConfig = do getModelInfoLogStatus' <- runGetModelInfoLogStatus modelInfoLogStatus <- liftIO getModelInfoLogStatus' let gqlMetrics = pmGraphQLRequestMetrics prometheusMetrics @@ -557,7 +560,7 @@ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicen runRemoteGQ fieldName rsi resultCustomizer gqlReq remoteJoins = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do (telemTimeIO_DT, remoteResponseHeaders, resp) <- doQErr $ E.execRemoteGQ env tracesPropagator userInfo reqHeaders (rsDef rsi) gqlReq - value <- extractFieldFromResponse fieldName resultCustomizer resp + value <- extractFieldFromResponse remoteSchemaResponsePriority fieldName resultCustomizer resp (finalResponse, modelInfo) <- doQErr $ RJ.processRemoteJoins @@ -663,34 +666,77 @@ coalescePostgresMutations plan = do _ -> Nothing Just (oneSourceConfig, oneResolvedConnectionTemplate, mutations) +data RemoteGraphQLResponse + = -- | "data" is omitted or `null` and "errors" is non-empty list + RGROnlyErrors (NonEmpty J.Value) + | -- | "data" is present and non-null, "errors" is omitted + RGROnlyData JO.Value + | -- | "data" is present and non-null, "errors" is non-empty list + RGRDataAndErrors JO.Value (NonEmpty J.Value) + data GraphQLResponse = GraphQLResponseErrors [J.Value] | GraphQLResponseData JO.Value -decodeGraphQLResponse :: LBS.ByteString -> Either Text GraphQLResponse -decodeGraphQLResponse bs = do +-- | This function decodes the response from a remote server: +-- +-- 1. First, errors are fetched from the response. Absence of errors field and `errors: null` both implies that there +-- are no errors. +-- 2. Next, the data field is fetched from the response. +-- 3. If a non-null data field is present in the response and there are no errors, then the data field is returned. +-- 4. If a non-null data field is not present in the response and there are errors, then the errors are thrown. +-- 5. If the data field is not present and there are no errors, then an error is thrown. +-- 6. If both data and errors are present, then we need to decide which one to pick based on the priority. +decodeGraphQLResponse :: RemoteSchemaResponsePriority -> LBS.ByteString -> Either Text GraphQLResponse +decodeGraphQLResponse remoteSchemaResponsePriority bs = do val <- mapLeft T.pack $ JO.eitherDecode bs - valObj <- JO.asObject val - case JO.lookup "errors" valObj of - Just (JO.Array errs) -> Right $ GraphQLResponseErrors (toList $ JO.fromOrdered <$> errs) - Just _ -> Left "Invalid \"errors\" field in response from remote" - Nothing -> do - dataVal <- JO.lookup "data" valObj `onNothing` Left "Missing \"data\" field in response from remote" - Right $ GraphQLResponseData dataVal + response <- buildRemoteGraphQLResponse val + case response of + RGROnlyErrors errs -> Right $ GraphQLResponseErrors $ toList errs + RGROnlyData d -> Right $ GraphQLResponseData d + RGRDataAndErrors d errs -> + -- Both data (non-null) and errors (non-empty) is present, we need to decide which one to pick based on the + -- priority + case remoteSchemaResponsePriority of + RemoteSchemaResponseData -> Right $ GraphQLResponseData d + RemoteSchemaResponseErrors -> Right $ GraphQLResponseErrors $ toList errs + +buildRemoteGraphQLResponse :: JO.Value -> Either Text RemoteGraphQLResponse +buildRemoteGraphQLResponse response = do + responseObj <- JO.asObject response + errors <- + case JO.lookup "errors" responseObj of + -- Absence of errors field and errors: null both implies that there are no errors + Just (JO.Array errs) -> do + neErrors <- maybeToEither "Empty \"errors\" field in response from remote" $ NE.nonEmpty $ Vec.toList errs + pure $ Just neErrors + Just JO.Null -> pure Nothing + Nothing -> pure Nothing + Just _ -> Left "Invalid \"errors\" field in response from remote" + case (JO.lookup "data" responseObj, errors) of + -- According to spec, If the data entry in the response is not present, the errors entry in the response must not be + -- empty. + (Nothing, Nothing) -> Left "Missing \"data\" field with no errors in response from remote" + (Nothing, Just nonEmptyErrors) -> Right $ RGROnlyErrors $ JO.fromOrdered <$> nonEmptyErrors + (Just JO.Null, Nothing) -> Left "Received null \"data\" field with no errors in response from remote" + (Just JO.Null, Just nonEmptyErrors) -> Right $ RGROnlyErrors $ JO.fromOrdered <$> nonEmptyErrors + (Just dataVal, Nothing) -> Right $ RGROnlyData dataVal + (Just dataVal, Just nonEmptyErrors) -> Right $ RGRDataAndErrors dataVal $ JO.fromOrdered <$> nonEmptyErrors extractFieldFromResponse :: forall m. (Monad m) => + RemoteSchemaResponsePriority -> RootFieldAlias -> ResultCustomizer -> LBS.ByteString -> ExceptT (Either GQExecError QErr) m JO.Value -extractFieldFromResponse fieldName resultCustomizer resp = do +extractFieldFromResponse remoteSchemaResponsePriority fieldName resultCustomizer resp = do let fieldName' = G.unName $ _rfaAlias fieldName dataVal <- applyResultCustomizer resultCustomizer <$> do - graphQLResponse <- decodeGraphQLResponse resp `onLeft` do400 + graphQLResponse <- decodeGraphQLResponse remoteSchemaResponsePriority resp `onLeft` do400 case graphQLResponse of GraphQLResponseErrors errs -> doGQExecError errs GraphQLResponseData d -> pure d @@ -747,6 +793,7 @@ runGQBatched :: Maybe (CredentialCache AgentLicenseKey) -> RequestId -> ResponseInternalErrorsConfig -> + RemoteSchemaResponsePriority -> UserInfo -> Wai.IpAddress -> [HTTP.Header] -> @@ -754,10 +801,10 @@ runGQBatched :: -- | the batched request with unparsed GraphQL query GQLBatchedReqs (GQLReq GQLQueryText) -> m (HttpLogGraphQLInfo, HttpResponse EncJSON) -runGQBatched env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId responseErrorsConfig userInfo ipAddress reqHdrs queryType query = +runGQBatched env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId responseErrorsConfig remoteSchemaResponsePriority userInfo ipAddress reqHdrs queryType query = case query of GQLSingleRequest req -> do - (gqlQueryOperationLog, httpResp) <- runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req responseErrorsConfig + (gqlQueryOperationLog, httpResp) <- runGQ env sqlGenCtx sc enableAL readOnlyMode remoteSchemaResponsePriority prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req responseErrorsConfig let httpLoggingGQInfo = (CommonHttpLogMetadata L.RequestModeSingle (Just (GQLSingleRequest (GQLQueryOperationSuccess gqlQueryOperationLog))), (PQHSetSingleton (gqolParameterizedQueryHash gqlQueryOperationLog))) pure (httpLoggingGQInfo, snd <$> httpResp) GQLBatchedReqs reqs -> do @@ -770,7 +817,7 @@ runGQBatched env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger age flip HttpResponse [] . encJFromList . map (either (encJFromJEncoding . encodeGQErr includeInternal) _hrBody) - responses <- for reqs \req -> fmap (req,) $ try $ (fmap . fmap . fmap) snd $ runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req responseErrorsConfig + responses <- for reqs \req -> fmap (req,) $ try $ (fmap . fmap . fmap) snd $ runGQ env sqlGenCtx sc enableAL readOnlyMode remoteSchemaResponsePriority prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req responseErrorsConfig let requestsOperationLogs = map fst $ rights $ map snd responses batchOperationLogs = map diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index ee29324cb1226..bd0c5e6da40f0 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -103,7 +103,7 @@ import Hasura.Server.Prometheus PrometheusMetrics (..), ) import Hasura.Server.Telemetry.Counters qualified as Telem -import Hasura.Server.Types (GranularPrometheusMetricsState (..), ModelInfoLogState (..), MonadGetPolicies (..), RequestId, getRequestId) +import Hasura.Server.Types (GranularPrometheusMetricsState (..), ModelInfoLogState (..), MonadGetPolicies (..), RemoteSchemaResponsePriority, RequestId, getRequestId) import Hasura.Services.Network import Hasura.Session import Hasura.Tracing qualified as Tracing @@ -483,6 +483,7 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables env <- liftIO $ acEnvironment <$> getAppContext appStateRef sqlGenCtx <- liftIO $ acSQLGenCtx <$> getAppContext appStateRef enableAL <- liftIO $ acEnableAllowlist <$> getAppContext appStateRef + remoteSchemaResponsePriority <- liftIO $ acRemoteSchemaResponsePriority <$> getAppContext appStateRef (reqParsed, queryParts) <- Tracing.newSpan "Parse GraphQL" $ do reqParsedE <- lift $ E.checkGQLExecution userInfo (reqHdrs, ipAddress) enableAL sc q requestId @@ -562,7 +563,7 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables pure $ (AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [], modelInfo) E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema - runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator + runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator remoteSchemaResponsePriority E.ExecStepAction actionExecPlan _ remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction (time, (resp, _), modelInfo) <- doQErr $ do @@ -654,7 +655,7 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables pure $ (AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs, modelInfo) E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema - runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator + runRemoteGQ requestId q fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator remoteSchemaResponsePriority E.ExecStepRaw json -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindIntrospection (,[]) <$> buildRaw json @@ -824,13 +825,14 @@ onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables GQLReqOutgoing -> Maybe RJ.RemoteJoins -> Tracing.HttpPropagator -> + RemoteSchemaResponsePriority -> ExceptT (Either GQExecError QErr) (ExceptT () m) (AnnotatedResponsePart, [ModelInfoPart]) - runRemoteGQ requestId reqUnparsed fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do + runRemoteGQ requestId reqUnparsed fieldName userInfo reqHdrs rsi resultCustomizer gqlReq remoteJoins tracesPropagator remoteSchemaResponsePriority = Tracing.newSpan ("Remote schema query for root field " <>> fieldName) $ do env <- liftIO $ acEnvironment <$> getAppContext appStateRef (telemTimeIO_DT, _respHdrs, resp) <- doQErr $ E.execRemoteGQ env tracesPropagator userInfo reqHdrs (rsDef rsi) gqlReq - value <- hoist lift $ extractFieldFromResponse fieldName resultCustomizer resp + value <- hoist lift $ extractFieldFromResponse remoteSchemaResponsePriority fieldName resultCustomizer resp (finalResponse, modelInfo) <- doQErr $ RJ.processRemoteJoins diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index a4bb956218e46..933627d54f077 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -588,7 +588,7 @@ v1Alpha1GQHandler queryType query = do reqHeaders <- asks hcReqHeaders ipAddress <- asks hcSourceIpAddress requestId <- asks hcRequestId - GH.runGQBatched acEnvironment acSQLGenCtx schemaCache acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId acResponseInternalErrorsConfig userInfo ipAddress reqHeaders queryType query + GH.runGQBatched acEnvironment acSQLGenCtx schemaCache acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId acResponseInternalErrorsConfig acRemoteSchemaResponsePriority userInfo ipAddress reqHeaders queryType query v1GQHandler :: ( MonadIO m, @@ -954,7 +954,7 @@ httpApp setupHook appStateRef AppEnv {..} consoleType ekgStore closeWebsocketsOn Spock.PATCH -> pure EP.PATCH other -> throw400 BadRequest $ "Method " <> tshow other <> " not supported." _ -> throw400 BadRequest $ "Nonstandard method not allowed for REST endpoints" - fmap JSONResp <$> runCustomEndpoint acEnvironment acSQLGenCtx schemaCache acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId userInfo reqHeaders ipAddress req endpoints responseErrorsConfig + fmap JSONResp <$> runCustomEndpoint acEnvironment acSQLGenCtx schemaCache acEnableAllowlist appEnvEnableReadOnlyMode acRemoteSchemaResponsePriority appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId userInfo reqHeaders ipAddress req endpoints responseErrorsConfig -- See Issue #291 for discussion around restified feature Spock.hookRouteAll ("api" "rest" Spock.wildcard) $ \wildcard -> do diff --git a/server/src-lib/Hasura/Server/Init.hs b/server/src-lib/Hasura/Server/Init.hs index 468f6fdec0e50..4ee780d16ff44 100644 --- a/server/src-lib/Hasura/Server/Init.hs +++ b/server/src-lib/Hasura/Server/Init.hs @@ -223,6 +223,7 @@ mkServeOptions sor@ServeOptionsRaw {..} = do soAsyncActionsFetchBatchSize <- withOptionDefault rsoAsyncActionsFetchBatchSize asyncActionsFetchBatchSizeOption soPersistedQueries <- withOptionDefault rsoPersistedQueries persistedQueriesOption soPersistedQueriesTtl <- withOptionDefault rsoPersistedQueriesTtl persistedQueriesTtlOption + soRemoteSchemaResponsePriority <- withOptionDefault rsoRemoteSchemaResponsePriority remoteSchemaResponsePriorityOption pure ServeOptions {..} -- | Fetch Postgres 'Query.ConnParams' components from the environment diff --git a/server/src-lib/Hasura/Server/Init/Arg/Command/Serve.hs b/server/src-lib/Hasura/Server/Init/Arg/Command/Serve.hs index 480e64a9bbf4c..e4aae9c2410fa 100644 --- a/server/src-lib/Hasura/Server/Init/Arg/Command/Serve.hs +++ b/server/src-lib/Hasura/Server/Init/Arg/Command/Serve.hs @@ -68,6 +68,7 @@ module Hasura.Server.Init.Arg.Command.Serve asyncActionsFetchBatchSizeOption, persistedQueriesOption, persistedQueriesTtlOption, + remoteSchemaResponsePriorityOption, -- * Pretty Printer serveCmdFooter, @@ -162,6 +163,7 @@ serveCommandParser = <*> parseAsyncActionsFetchBatchSize <*> parsePersistedQueries <*> parsePersistedQueriesTtl + <*> parseRemoteSchemaResponsePriority -------------------------------------------------------------------------------- -- Serve Options @@ -1311,6 +1313,22 @@ parsePersistedQueriesTtl = <> Opt.help (Config._helpMessage persistedQueriesTtlOption) ) +remoteSchemaResponsePriorityOption :: Config.Option (Types.RemoteSchemaResponsePriority) +remoteSchemaResponsePriorityOption = + Config.Option + { Config._default = Types.RemoteSchemaResponseErrors, + Config._envVar = "HASURA_GRAPHQL_REMOTE_SCHEMA_PRIORITIZE_DATA", + Config._helpMessage = "Prioritize data over errors for remote schema responses (default: false)." + } + +parseRemoteSchemaResponsePriority :: Opt.Parser (Maybe Types.RemoteSchemaResponsePriority) +parseRemoteSchemaResponsePriority = + (bool Nothing (Just Types.RemoteSchemaResponseData)) + <$> Opt.switch + ( Opt.long "remote-schema-prioritize-data" + <> Opt.help (Config._helpMessage remoteSchemaResponsePriorityOption) + ) + -------------------------------------------------------------------------------- -- Pretty Printer diff --git a/server/src-lib/Hasura/Server/Init/Config.hs b/server/src-lib/Hasura/Server/Init/Config.hs index eb2b9839d4f43..8f1b793b97f72 100644 --- a/server/src-lib/Hasura/Server/Init/Config.hs +++ b/server/src-lib/Hasura/Server/Init/Config.hs @@ -328,7 +328,8 @@ data ServeOptionsRaw impl = ServeOptionsRaw rsoTriggersErrorLogLevelStatus :: Maybe Server.Types.TriggersErrorLogLevelStatus, rsoAsyncActionsFetchBatchSize :: Maybe Int, rsoPersistedQueries :: Maybe Server.Types.PersistedQueriesState, - rsoPersistedQueriesTtl :: Maybe Int + rsoPersistedQueriesTtl :: Maybe Int, + rsoRemoteSchemaResponsePriority :: Maybe Server.Types.RemoteSchemaResponsePriority } -- | Whether or not to serve Console assets. @@ -634,7 +635,8 @@ data ServeOptions impl = ServeOptions soTriggersErrorLogLevelStatus :: Server.Types.TriggersErrorLogLevelStatus, soAsyncActionsFetchBatchSize :: Int, soPersistedQueries :: Server.Types.PersistedQueriesState, - soPersistedQueriesTtl :: Int + soPersistedQueriesTtl :: Int, + soRemoteSchemaResponsePriority :: Server.Types.RemoteSchemaResponsePriority } -- | 'ResponseInternalErrorsConfig' represents the encoding of the diff --git a/server/src-lib/Hasura/Server/Init/Env.hs b/server/src-lib/Hasura/Server/Init/Env.hs index becfe3f299823..36c175d313179 100644 --- a/server/src-lib/Hasura/Server/Init/Env.hs +++ b/server/src-lib/Hasura/Server/Init/Env.hs @@ -387,3 +387,6 @@ instance FromEnv Server.Types.TriggersErrorLogLevelStatus where instance FromEnv Server.Types.PersistedQueriesState where fromEnv = fmap (bool Server.Types.PersistedQueriesDisabled Server.Types.PersistedQueriesEnabled) . fromEnv @Bool + +instance FromEnv Server.Types.RemoteSchemaResponsePriority where + fromEnv = fmap (bool Server.Types.RemoteSchemaResponseErrors Server.Types.RemoteSchemaResponseData) . fromEnv @Bool diff --git a/server/src-lib/Hasura/Server/Rest.hs b/server/src-lib/Hasura/Server/Rest.hs index cf36c4f40822e..36f2cbcb67173 100644 --- a/server/src-lib/Hasura/Server/Rest.hs +++ b/server/src-lib/Hasura/Server/Rest.hs @@ -118,6 +118,7 @@ runCustomEndpoint :: SchemaCache -> Init.AllowListStatus -> ReadOnlyMode -> + RemoteSchemaResponsePriority -> PrometheusMetrics -> L.Logger L.Hasura -> Maybe (CredentialCache AgentLicenseKey) -> @@ -129,7 +130,7 @@ runCustomEndpoint :: EndpointTrie GQLQueryWithText -> Init.ResponseInternalErrorsConfig -> m (HttpLogGraphQLInfo, HttpResponse EncJSON) -runCustomEndpoint env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey requestId userInfo reqHeaders ipAddress RestRequest {..} endpoints responseErrorsConfig = do +runCustomEndpoint env sqlGenCtx sc enableAL readOnlyMode remoteSchemaResponsePriority prometheusMetrics logger agentLicenseKey requestId userInfo reqHeaders ipAddress RestRequest {..} endpoints responseErrorsConfig = do -- First match the path to an endpoint. case matchPath reqMethod (T.split (== '/') reqPath) endpoints of MatchFound (queryx :: EndpointMetadata GQLQueryWithText) matches -> @@ -159,7 +160,7 @@ runCustomEndpoint env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logge -- with the query string from the schema cache, and pass it -- through to the /v1/graphql endpoint. (httpLoggingMetadata, handlerResp) <- do - (gqlOperationLog, resp) <- GH.runGQ env sqlGenCtx sc enableAL readOnlyMode prometheusMetrics logger agentLicenseKey requestId userInfo ipAddress reqHeaders E.QueryHasura (mkPassthroughRequest queryx resolvedVariables) responseErrorsConfig + (gqlOperationLog, resp) <- GH.runGQ env sqlGenCtx sc enableAL readOnlyMode remoteSchemaResponsePriority prometheusMetrics logger agentLicenseKey requestId userInfo ipAddress reqHeaders E.QueryHasura (mkPassthroughRequest queryx resolvedVariables) responseErrorsConfig let httpLoggingGQInfo = (CommonHttpLogMetadata RequestModeNonBatchable Nothing, (PQHSetSingleton (gqolParameterizedQueryHash gqlOperationLog))) return (httpLoggingGQInfo, fst <$> resp) case sequence handlerResp of diff --git a/server/src-lib/Hasura/Server/Types.hs b/server/src-lib/Hasura/Server/Types.hs index 33669272e9c15..190456419c0c0 100644 --- a/server/src-lib/Hasura/Server/Types.hs +++ b/server/src-lib/Hasura/Server/Types.hs @@ -31,6 +31,7 @@ module Hasura.Server.Types ExtPersistedQueryRequest (..), ExtQueryReqs (..), MonadGetPolicies (..), + RemoteSchemaResponsePriority (..), ) where @@ -360,3 +361,17 @@ instance (MonadGetPolicies m) => MonadGetPolicies (StateT w m) where runGetApiTimeLimit = lift runGetApiTimeLimit runGetPrometheusMetricsGranularity = lift runGetPrometheusMetricsGranularity runGetModelInfoLogStatus = lift $ runGetModelInfoLogStatus + +-- | The priority of the response to be sent to the client for remote schema fields if there is both errors as well as +-- data in the remote response. +-- Read more about how we decode the remote response at `decodeGraphQLResp` in `Hasura.GraphQL.Transport.HTTP` +-- +-- If there is both errors and data in the remote response, then: +-- +-- * If the priority is set to `RemoteSchemaResponseData`, then the data is sent to the client. +-- * If the priority is set to `RemoteSchemaResponseErrors`, then the errors are sent to the client. +data RemoteSchemaResponsePriority + = -- | Data from the remote schema is sent + RemoteSchemaResponseData + | -- | Errors from the remote schema is sent + RemoteSchemaResponseErrors diff --git a/server/src-test/Hasura/Server/InitSpec.hs b/server/src-test/Hasura/Server/InitSpec.hs index f80f8b82e887b..d9e2361d86e44 100644 --- a/server/src-test/Hasura/Server/InitSpec.hs +++ b/server/src-test/Hasura/Server/InitSpec.hs @@ -99,7 +99,8 @@ emptyServeOptionsRaw = rsoTriggersErrorLogLevelStatus = Nothing, rsoAsyncActionsFetchBatchSize = Nothing, rsoPersistedQueries = Nothing, - rsoPersistedQueriesTtl = Nothing + rsoPersistedQueriesTtl = Nothing, + rsoRemoteSchemaResponsePriority = Nothing } mkServeOptionsSpec :: Hspec.Spec diff --git a/server/test-postgres/Constants.hs b/server/test-postgres/Constants.hs index 7a6e710e32e35..0a074f34f8abd 100644 --- a/server/test-postgres/Constants.hs +++ b/server/test-postgres/Constants.hs @@ -98,7 +98,8 @@ serveOptions = soTriggersErrorLogLevelStatus = Init._default Init.triggersErrorLogLevelStatusOption, soAsyncActionsFetchBatchSize = Init._default Init.asyncActionsFetchBatchSizeOption, soPersistedQueries = Init._default Init.persistedQueriesOption, - soPersistedQueriesTtl = Init._default Init.persistedQueriesTtlOption + soPersistedQueriesTtl = Init._default Init.persistedQueriesTtlOption, + soRemoteSchemaResponsePriority = Init._default Init.remoteSchemaResponsePriorityOption } -- | What log level should be used by the engine; this is not exported, and diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_and_errors_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_and_errors_query.yaml new file mode 100644 index 0000000000000..91d1589379d1e --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_and_errors_query.yaml @@ -0,0 +1,22 @@ +- description: Query from the remote schema returning errors and data + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: DataAndError + response: + data: + test: + - id: 1 + created_at: '2023-12-04T08:14:52.1851+00:00' + - id: 2 + created_at: '2023-12-04T08:14:52.680052+00:00' + - id: 3 + created_at: '2023-12-04T08:14:53.335059+00:00' + query: + query: | + query DataAndError { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_only_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_only_query.yaml new file mode 100644 index 0000000000000..7e7dd04c15ec3 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_data_only_query.yaml @@ -0,0 +1,22 @@ +- description: Query from the remote schema returning data only + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: DataOnly + response: + data: + test: + - id: 1 + created_at: '2023-12-04T08:14:52.1851+00:00' + - id: 2 + created_at: '2023-12-04T08:14:52.680052+00:00' + - id: 3 + created_at: '2023-12-04T08:14:53.335059+00:00' + query: + query: | + query DataOnly { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_error_only_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_error_only_query.yaml new file mode 100644 index 0000000000000..687743b3e0af1 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/data_prioritization/test_error_only_query.yaml @@ -0,0 +1,20 @@ +- description: Query from the remote schema returning errors only + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: ErrorOnly + response: + data: + errors: + - extensions: + code: validation-failed + path: "$.selectionSet.test_lol" + message: 'field ''test'' not found in type: ''query_root''' + query: + query: | + query ErrorOnly { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_and_errors_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_and_errors_query.yaml new file mode 100644 index 0000000000000..4a955bf585c2b --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_and_errors_query.yaml @@ -0,0 +1,20 @@ +- description: Query from the remote schema returning errors and data + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: DataAndError + response: + data: + errors: + - extensions: + code: validation-failed + path: "$.selectionSet.test_lol" + message: 'field ''test'' not found in type: ''query_root''' + query: + query: | + query DataAndError { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_only_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_only_query.yaml new file mode 100644 index 0000000000000..7e7dd04c15ec3 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_data_only_query.yaml @@ -0,0 +1,22 @@ +- description: Query from the remote schema returning data only + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: DataOnly + response: + data: + test: + - id: 1 + created_at: '2023-12-04T08:14:52.1851+00:00' + - id: 2 + created_at: '2023-12-04T08:14:52.680052+00:00' + - id: 3 + created_at: '2023-12-04T08:14:53.335059+00:00' + query: + query: | + query DataOnly { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_error_only_query.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_error_only_query.yaml new file mode 100644 index 0000000000000..687743b3e0af1 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/no_prioritization/test_error_only_query.yaml @@ -0,0 +1,20 @@ +- description: Query from the remote schema returning errors only + url: /v1/graphql + status: 200 + headers: + x-fake-operation-name: ErrorOnly + response: + data: + errors: + - extensions: + code: validation-failed + path: "$.selectionSet.test_lol" + message: 'field ''test'' not found in type: ''query_root''' + query: + query: | + query ErrorOnly { + test { + id + created_at + } + } diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/setup.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/setup.yaml new file mode 100644 index 0000000000000..e6c4720d686d5 --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/setup.yaml @@ -0,0 +1,6 @@ +type: add_remote_schema +args: + name: my-remote-schema + definition: + url: "{{GRAPHQL_SERVICE}}" + forward_client_headers: true diff --git a/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/teardown.yaml b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/teardown.yaml new file mode 100644 index 0000000000000..f3fc2f08e9d6c --- /dev/null +++ b/server/tests-py/queries/remote_schemas/validate_data_errors_prioritization/teardown.yaml @@ -0,0 +1,3 @@ +type: remove_remote_schema +args: + name: my-remote-schema diff --git a/server/tests-py/remote_schemas/nodejs/returns_data_and_errors.js b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors.js new file mode 100644 index 0000000000000..78c9ff9a3c751 --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors.js @@ -0,0 +1,39 @@ +const express = require('express'); +const path = require("path"); +const fs = require('fs'); +const app = express(); +app.use(express.json()); + +app.post('/', (req, res) => { + let fileName; + switch(req.body.operationName) { + case "IntrospectionQuery": + fileName = 'introspection.json'; + break; + default: + switch(req.header('x-fake-operation-name')) { + case "DataOnly": + fileName = 'data_only.json'; + break; + case "ErrorOnly": + fileName = 'error_only.json'; + break; + case "DataAndError": + fileName = 'data_and_error.json'; + break; + default: + throw new Error("expected a header `x-fake-operation-name` to be from the list [DataOnly, ErrorOnly, DataAndError]"); + } + } + fs.readFile(path.resolve(__dirname, 'returns_data_and_errors_responses', fileName), 'utf8', (err, data) => { + if (err) { + console.error(err); + return; + } + res.json(JSON.parse(data)); + }); +}); +let port = process.env.PORT || 4000; +app.listen(port, () => { + console.log(`🚀 Server ready at http://localhost::${port}`); +}); diff --git a/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_and_error.json b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_and_error.json new file mode 100644 index 0000000000000..19f05d09f5e6a --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_and_error.json @@ -0,0 +1,27 @@ +{ + "data": { + "test": [ + { + "id": 1, + "created_at": "2023-12-04T08:14:52.1851+00:00" + }, + { + "id": 2, + "created_at": "2023-12-04T08:14:52.680052+00:00" + }, + { + "id": 3, + "created_at": "2023-12-04T08:14:53.335059+00:00" + } + ] + }, + "errors": [ + { + "message": "field 'test' not found in type: 'query_root'", + "extensions": { + "path": "$.selectionSet.test_lol", + "code": "validation-failed" + } + } + ] +} diff --git a/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_only.json b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_only.json new file mode 100644 index 0000000000000..06ce49e42cc3e --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/data_only.json @@ -0,0 +1,18 @@ +{ + "data": { + "test": [ + { + "id": 1, + "created_at": "2023-12-04T08:14:52.1851+00:00" + }, + { + "id": 2, + "created_at": "2023-12-04T08:14:52.680052+00:00" + }, + { + "id": 3, + "created_at": "2023-12-04T08:14:53.335059+00:00" + } + ] + } +} diff --git a/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/error_only.json b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/error_only.json new file mode 100644 index 0000000000000..c7dda9c67d1d7 --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/error_only.json @@ -0,0 +1,11 @@ +{ + "errors": [ + { + "message": "field 'test' not found in type: 'query_root'", + "extensions": { + "path": "$.selectionSet.test_lol", + "code": "validation-failed" + } + } + ] +} diff --git a/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/introspection.json b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/introspection.json new file mode 100644 index 0000000000000..7dedf2650a5c6 --- /dev/null +++ b/server/tests-py/remote_schemas/nodejs/returns_data_and_errors_responses/introspection.json @@ -0,0 +1 @@ +{"data": {"__schema": {"queryType": {"name": "query_root"}, "mutationType": {"name": "mutation_root"}, "subscriptionType": {"name": "subscription_root"}, "types": [{"kind": "SCALAR", "name": "Boolean", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Float", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "Int", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "Int_comparison_exp", "description": "Boolean expression to compare columns of type \"Int\". All fields are combined with logical 'AND'.", "fields": null, "inputFields": [{"name": "_eq", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_gt", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_gte", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_in", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}}, "defaultValue": null}, {"name": "_is_null", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": null}, {"name": "_lt", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_lte", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_neq", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "_nin", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "String", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Directive", "description": null, "fields": [{"name": "args", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "isRepeatable", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "locations", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__EnumValue", "description": null, "fields": [{"name": "deprecationReason", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Field", "description": null, "fields": [{"name": "args", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "deprecationReason", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "isDeprecated", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__InputValue", "description": null, "fields": [{"name": "defaultValue", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "type", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Schema", "description": null, "fields": [{"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "directives", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Directive", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "mutationType", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "queryType", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "subscriptionType", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "types", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "__Type", "description": null, "fields": [{"name": "description", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "enumValues", "description": null, "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "OBJECT", "name": "__EnumValue", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "fields", "description": null, "args": [{"name": "includeDeprecated", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": "false"}], "type": {"kind": "OBJECT", "name": "__Field", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "inputFields", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__InputValue", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "interfaces", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "kind", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "__TypeKind", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "name", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "String", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "ofType", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "possibleTypes", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "__Type", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "__TypeKind", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ENUM", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INPUT_OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "INTERFACE", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "LIST", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "NON_NULL", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "OBJECT", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "SCALAR", "description": null, "isDeprecated": false, "deprecationReason": null}, {"name": "UNION", "description": null, "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "ENUM", "name": "cursor_ordering", "description": "ordering argument of a cursor", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "ASC", "description": "ascending ordering of the cursor", "isDeprecated": false, "deprecationReason": null}, {"name": "DESC", "description": "descending ordering of the cursor", "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "mutation_root", "description": "mutation root", "fields": [{"name": "delete_test", "description": "delete data from the table: \"test\"", "args": [{"name": "where", "description": "filter the rows which have to be deleted", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test_mutation_response", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "delete_test_by_pk", "description": "delete single row from the table: \"test\"", "args": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "insert_test", "description": "insert data into the table: \"test\"", "args": [{"name": "objects", "description": "the rows to be inserted", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_insert_input", "ofType": null}}}}, "defaultValue": null}, {"name": "on_conflict", "description": "upsert condition", "type": {"kind": "INPUT_OBJECT", "name": "test_on_conflict", "ofType": null}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test_mutation_response", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "insert_test_one", "description": "insert a single row into the table: \"test\"", "args": [{"name": "object", "description": "the row to be inserted", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_insert_input", "ofType": null}}, "defaultValue": null}, {"name": "on_conflict", "description": "upsert condition", "type": {"kind": "INPUT_OBJECT", "name": "test_on_conflict", "ofType": null}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "update_test", "description": "update data of the table: \"test\"", "args": [{"name": "_inc", "description": "increments the numeric columns with given value of the filtered values", "type": {"kind": "INPUT_OBJECT", "name": "test_inc_input", "ofType": null}, "defaultValue": null}, {"name": "_set", "description": "sets the columns of the filtered rows to the given values", "type": {"kind": "INPUT_OBJECT", "name": "test_set_input", "ofType": null}, "defaultValue": null}, {"name": "where", "description": "filter the rows which have to be updated", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test_mutation_response", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "update_test_by_pk", "description": "update single row of the table: \"test\"", "args": [{"name": "_inc", "description": "increments the numeric columns with given value of the filtered values", "type": {"kind": "INPUT_OBJECT", "name": "test_inc_input", "ofType": null}, "defaultValue": null}, {"name": "_set", "description": "sets the columns of the filtered rows to the given values", "type": {"kind": "INPUT_OBJECT", "name": "test_set_input", "ofType": null}, "defaultValue": null}, {"name": "pk_columns", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_pk_columns_input", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "update_test_many", "description": "update multiples rows of table: \"test\"", "args": [{"name": "updates", "description": "updates to execute, in order", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_updates", "ofType": null}}}}, "defaultValue": null}], "type": {"kind": "LIST", "name": null, "ofType": {"kind": "OBJECT", "name": "test_mutation_response", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "order_by", "description": "column ordering options", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "asc", "description": "in ascending order, nulls last", "isDeprecated": false, "deprecationReason": null}, {"name": "asc_nulls_first", "description": "in ascending order, nulls first", "isDeprecated": false, "deprecationReason": null}, {"name": "asc_nulls_last", "description": "in ascending order, nulls last", "isDeprecated": false, "deprecationReason": null}, {"name": "desc", "description": "in descending order, nulls first", "isDeprecated": false, "deprecationReason": null}, {"name": "desc_nulls_first", "description": "in descending order, nulls first", "isDeprecated": false, "deprecationReason": null}, {"name": "desc_nulls_last", "description": "in descending order, nulls last", "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "OBJECT", "name": "query_root", "description": null, "fields": [{"name": "test", "description": "fetch data from the table: \"test\"", "args": [{"name": "distinct_on", "description": "distinct select on columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_select_column", "ofType": null}}}, "defaultValue": null}, {"name": "limit", "description": "limit the number of rows returned", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "offset", "description": "skip the first n rows. Use only with order_by", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "order_by", "description": "sort the rows by one or more columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_order_by", "ofType": null}}}, "defaultValue": null}, {"name": "where", "description": "filter the rows returned", "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "test_aggregate", "description": "fetch aggregated fields from the table: \"test\"", "args": [{"name": "distinct_on", "description": "distinct select on columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_select_column", "ofType": null}}}, "defaultValue": null}, {"name": "limit", "description": "limit the number of rows returned", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "offset", "description": "skip the first n rows. Use only with order_by", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "order_by", "description": "sort the rows by one or more columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_order_by", "ofType": null}}}, "defaultValue": null}, {"name": "where", "description": "filter the rows returned", "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test_aggregate", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "test_by_pk", "description": "fetch data from the table: \"test\" using primary key columns", "args": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "subscription_root", "description": null, "fields": [{"name": "test", "description": "fetch data from the table: \"test\"", "args": [{"name": "distinct_on", "description": "distinct select on columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_select_column", "ofType": null}}}, "defaultValue": null}, {"name": "limit", "description": "limit the number of rows returned", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "offset", "description": "skip the first n rows. Use only with order_by", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "order_by", "description": "sort the rows by one or more columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_order_by", "ofType": null}}}, "defaultValue": null}, {"name": "where", "description": "filter the rows returned", "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}, {"name": "test_aggregate", "description": "fetch aggregated fields from the table: \"test\"", "args": [{"name": "distinct_on", "description": "distinct select on columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_select_column", "ofType": null}}}, "defaultValue": null}, {"name": "limit", "description": "limit the number of rows returned", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "offset", "description": "skip the first n rows. Use only with order_by", "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}, {"name": "order_by", "description": "sort the rows by one or more columns", "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_order_by", "ofType": null}}}, "defaultValue": null}, {"name": "where", "description": "filter the rows returned", "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test_aggregate", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "test_by_pk", "description": "fetch data from the table: \"test\" using primary key columns", "args": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}], "type": {"kind": "OBJECT", "name": "test", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "test_stream", "description": "fetch data from the table in a streaming manner: \"test\"", "args": [{"name": "batch_size", "description": "maximum number of rows returned in a single batch", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}, {"name": "cursor", "description": "cursor to stream the results returned by the query", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_stream_cursor_input", "ofType": null}}}, "defaultValue": null}, {"name": "where", "description": "filter the rows returned", "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test", "description": "columns and relationships of \"test\"", "fields": [{"name": "created_at", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "id", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_aggregate", "description": "aggregated selection of \"test\"", "fields": [{"name": "aggregate", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_aggregate_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "nodes", "description": null, "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_aggregate_fields", "description": "aggregate fields of \"test\"", "fields": [{"name": "avg", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_avg_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "count", "description": null, "args": [{"name": "columns", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_select_column", "ofType": null}}}, "defaultValue": null}, {"name": "distinct", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": null}], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "max", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_max_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "min", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_min_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "stddev", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_stddev_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "stddev_pop", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_stddev_pop_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "stddev_samp", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_stddev_samp_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "sum", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_sum_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "var_pop", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_var_pop_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "var_samp", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_var_samp_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "variance", "description": null, "args": [], "type": {"kind": "OBJECT", "name": "test_variance_fields", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_avg_fields", "description": "aggregate avg on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "description": "Boolean expression to filter rows from the table \"test\". All fields are combined with a logical 'AND'.", "fields": null, "inputFields": [{"name": "_and", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}}}, "defaultValue": null}, {"name": "_not", "description": null, "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}, {"name": "_or", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}}}, "defaultValue": null}, {"name": "created_at", "description": null, "type": {"kind": "INPUT_OBJECT", "name": "timestamptz_comparison_exp", "ofType": null}, "defaultValue": null}, {"name": "id", "description": null, "type": {"kind": "INPUT_OBJECT", "name": "Int_comparison_exp", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "test_constraint", "description": "unique or primary key constraints on table \"test\"", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "test_pkey", "description": "unique or primary key constraint on columns \"id\"", "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_inc_input", "description": "input type for incrementing numeric columns in table \"test\"", "fields": null, "inputFields": [{"name": "id", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_insert_input", "description": "input type for inserting data into table \"test\"", "fields": null, "inputFields": [{"name": "created_at", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "id", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_max_fields", "description": "aggregate max on columns", "fields": [{"name": "created_at", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_min_fields", "description": "aggregate min on columns", "fields": [{"name": "created_at", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "isDeprecated": false, "deprecationReason": null}, {"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_mutation_response", "description": "response of any mutation on the table \"test\"", "fields": [{"name": "affected_rows", "description": "number of rows affected by the mutation", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "isDeprecated": false, "deprecationReason": null}, {"name": "returning", "description": "data from the rows affected by the mutation", "args": [], "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "OBJECT", "name": "test", "ofType": null}}}}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_on_conflict", "description": "on_conflict condition type for table \"test\"", "fields": null, "inputFields": [{"name": "constraint", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_constraint", "ofType": null}}, "defaultValue": null}, {"name": "update_columns", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "ENUM", "name": "test_update_column", "ofType": null}}}}, "defaultValue": "[]"}, {"name": "where", "description": null, "type": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_order_by", "description": "Ordering options when selecting data from \"test\".", "fields": null, "inputFields": [{"name": "created_at", "description": null, "type": {"kind": "ENUM", "name": "order_by", "ofType": null}, "defaultValue": null}, {"name": "id", "description": null, "type": {"kind": "ENUM", "name": "order_by", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_pk_columns_input", "description": "primary key columns input for table: test", "fields": null, "inputFields": [{"name": "id", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "test_select_column", "description": "select columns of table \"test\"", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "created_at", "description": "column name", "isDeprecated": false, "deprecationReason": null}, {"name": "id", "description": "column name", "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_set_input", "description": "input type for updating data in table \"test\"", "fields": null, "inputFields": [{"name": "created_at", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "id", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_stddev_fields", "description": "aggregate stddev on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_stddev_pop_fields", "description": "aggregate stddev_pop on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_stddev_samp_fields", "description": "aggregate stddev_samp on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_stream_cursor_input", "description": "Streaming cursor of the table \"test\"", "fields": null, "inputFields": [{"name": "initial_value", "description": "Stream column input with initial value", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_stream_cursor_value_input", "ofType": null}}, "defaultValue": null}, {"name": "ordering", "description": "cursor ordering", "type": {"kind": "ENUM", "name": "cursor_ordering", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_stream_cursor_value_input", "description": "Initial value of the column from where the streaming should start", "fields": null, "inputFields": [{"name": "created_at", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "id", "description": null, "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_sum_fields", "description": "aggregate sum on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Int", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "ENUM", "name": "test_update_column", "description": "update columns of table \"test\"", "fields": null, "inputFields": null, "interfaces": null, "enumValues": [{"name": "created_at", "description": "column name", "isDeprecated": false, "deprecationReason": null}, {"name": "id", "description": "column name", "isDeprecated": false, "deprecationReason": null}], "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "test_updates", "description": null, "fields": null, "inputFields": [{"name": "_inc", "description": "increments the numeric columns with given value of the filtered values", "type": {"kind": "INPUT_OBJECT", "name": "test_inc_input", "ofType": null}, "defaultValue": null}, {"name": "_set", "description": "sets the columns of the filtered rows to the given values", "type": {"kind": "INPUT_OBJECT", "name": "test_set_input", "ofType": null}, "defaultValue": null}, {"name": "where", "description": "filter the rows which have to be updated", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "INPUT_OBJECT", "name": "test_bool_exp", "ofType": null}}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_var_pop_fields", "description": "aggregate var_pop on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_var_samp_fields", "description": "aggregate var_samp on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "OBJECT", "name": "test_variance_fields", "description": "aggregate variance on columns", "fields": [{"name": "id", "description": null, "args": [], "type": {"kind": "SCALAR", "name": "Float", "ofType": null}, "isDeprecated": false, "deprecationReason": null}], "inputFields": null, "interfaces": [], "enumValues": null, "possibleTypes": null}, {"kind": "SCALAR", "name": "timestamptz", "description": null, "fields": null, "inputFields": null, "interfaces": null, "enumValues": null, "possibleTypes": null}, {"kind": "INPUT_OBJECT", "name": "timestamptz_comparison_exp", "description": "Boolean expression to compare columns of type \"timestamptz\". All fields are combined with logical 'AND'.", "fields": null, "inputFields": [{"name": "_eq", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_gt", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_gte", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_in", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}}}, "defaultValue": null}, {"name": "_is_null", "description": null, "type": {"kind": "SCALAR", "name": "Boolean", "ofType": null}, "defaultValue": null}, {"name": "_lt", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_lte", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_neq", "description": null, "type": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}, "defaultValue": null}, {"name": "_nin", "description": null, "type": {"kind": "LIST", "name": null, "ofType": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "timestamptz", "ofType": null}}}, "defaultValue": null}], "interfaces": null, "enumValues": null, "possibleTypes": null}], "directives": [{"name": "include", "description": "whether this query should be included", "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [{"name": "if", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "defaultValue": null}]}, {"name": "skip", "description": "whether this query should be skipped", "locations": ["FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT"], "args": [{"name": "if", "description": null, "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "defaultValue": null}]}, {"name": "cached", "description": "whether this query should be cached (Hasura Cloud only)", "locations": ["QUERY"], "args": [{"name": "ttl", "description": "measured in seconds", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Int", "ofType": null}}, "defaultValue": "60"}, {"name": "refresh", "description": "refresh the cache entry", "type": {"kind": "NON_NULL", "name": null, "ofType": {"kind": "SCALAR", "name": "Boolean", "ofType": null}}, "defaultValue": "false"}]}]}}} diff --git a/server/tests-py/test_remote_schema_prioritize_data.py b/server/tests-py/test_remote_schema_prioritize_data.py new file mode 100644 index 0000000000000..a2bf7e077c979 --- /dev/null +++ b/server/tests-py/test_remote_schema_prioritize_data.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import pytest + +from conftest import extract_server_address_from +from remote_server import NodeGraphQL +from validate import check_query_f + +pytestmark = [ + pytest.mark.admin_secret, + pytest.mark.hge_env('HASURA_GRAPHQL_REMOTE_SCHEMA_PRIORITIZE_DATA', 'true'), +] + +@pytest.fixture(scope='class') +@pytest.mark.early +def fake_graphql_service(worker_id: str, hge_fixture_env: dict[str, str]): + (_, port) = extract_server_address_from('GRAPHQL_SERVICE') + server = NodeGraphQL(worker_id, 'remote_schemas/nodejs/returns_data_and_errors.js', port=port) + server.start() + print(f'{fake_graphql_service.__name__} server started on {server.url}') + hge_fixture_env['GRAPHQL_SERVICE'] = server.url + yield server + server.stop() + +use_test_fixtures = pytest.mark.usefixtures( + 'fake_graphql_service', + 'per_method_tests_db_state', +) + +@use_test_fixtures +class TestRemoteSchemaPrioritizeData: + + @classmethod + def dir(cls): + return "queries/remote_schemas/validate_data_errors_prioritization/" + + def test_data_only_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'data_prioritization/test_data_only_query.yaml') + + def test_error_only_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'data_prioritization/test_error_only_query.yaml') + + def test_data_and_errors_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'data_prioritization/test_data_and_errors_query.yaml') diff --git a/server/tests-py/test_remote_schema_prioritize_none.py b/server/tests-py/test_remote_schema_prioritize_none.py new file mode 100644 index 0000000000000..05ced79ad6c0c --- /dev/null +++ b/server/tests-py/test_remote_schema_prioritize_none.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import pytest + +from conftest import extract_server_address_from +from remote_server import NodeGraphQL +from validate import check_query_f + +pytestmark = [ + pytest.mark.admin_secret, +] + +@pytest.fixture(scope='class') +@pytest.mark.early +def fake_graphql_service(worker_id: str, hge_fixture_env: dict[str, str]): + (_, port) = extract_server_address_from('GRAPHQL_SERVICE') + server = NodeGraphQL(worker_id, 'remote_schemas/nodejs/returns_data_and_errors.js', port=port) + server.start() + print(f'{fake_graphql_service.__name__} server started on {server.url}') + hge_fixture_env['GRAPHQL_SERVICE'] = server.url + yield server + server.stop() + +use_test_fixtures = pytest.mark.usefixtures( + 'fake_graphql_service', + 'per_method_tests_db_state', +) + +@use_test_fixtures +class TestRemoteSchemaPrioritizeErrors: + + @classmethod + def dir(cls): + return "queries/remote_schemas/validate_data_errors_prioritization/" + + def test_data_only_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'no_prioritization/test_data_only_query.yaml') + + def test_error_only_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'no_prioritization/test_error_only_query.yaml') + + def test_data_and_errors_query(self, hge_ctx): + check_query_f(hge_ctx, self.dir() + 'no_prioritization/test_data_and_errors_query.yaml')