mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-13 19:33:55 +03:00
server: add support for custom scalar in action output types
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4185 GitOrigin-RevId: 16a24fdcdbd195b4b59bcca7957c469ba073dabb
This commit is contained in:
parent
6586053296
commit
056765578f
@ -109,6 +109,7 @@ Response 2:
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- server: add support for custom scalar in action output type (#4185)
|
||||
- server: add support for MSSQL event triggers (#7228)
|
||||
- server: update pg_dump to be compatible with postgres 14 (#7676)
|
||||
- server: fix bug where timestamp values sent to postgres would erroneously trim leading zeroes (#8096)
|
||||
|
@ -39,6 +39,7 @@ module Hasura.Backends.Postgres.SQL.Types
|
||||
qualifiedObjectToName,
|
||||
PGScalarType (..),
|
||||
textToPGScalarType,
|
||||
pgScalarTranslations,
|
||||
pgScalarTypeToText,
|
||||
PGTypeKind (..),
|
||||
QualifiedPGType (..),
|
||||
|
@ -142,7 +142,7 @@ resolveActionExecution ::
|
||||
Maybe GQLQueryText ->
|
||||
ActionExecution
|
||||
resolveActionExecution env logger _userInfo IR.AnnActionExecution {..} ActionExecContext {..} gqlQueryText =
|
||||
ActionExecution $ first (encJFromOrderedValue . makeActionResponseNoRelations _aaeFields) <$> runWebhook
|
||||
ActionExecution $ first (encJFromOrderedValue . makeActionResponseNoRelations _aaeFields _aaeOutputType _aaeOutputFields True) <$> runWebhook
|
||||
where
|
||||
handlerPayload = ActionWebhookPayload (ActionContext _aaeName) _aecSessionVariables _aaePayload gqlQueryText
|
||||
|
||||
@ -166,8 +166,8 @@ resolveActionExecution env logger _userInfo IR.AnnActionExecution {..} ActionExe
|
||||
_aaeResponseTransform
|
||||
|
||||
-- | Build action response from the Webhook JSON response when there are no relationships defined
|
||||
makeActionResponseNoRelations :: IR.ActionFields -> ActionWebhookResponse -> AO.Value
|
||||
makeActionResponseNoRelations annFields webhookResponse =
|
||||
makeActionResponseNoRelations :: IR.ActionFields -> GraphQLType -> IR.ActionOutputFields -> Bool -> ActionWebhookResponse -> AO.Value
|
||||
makeActionResponseNoRelations annFields outputType outputF shouldCheckOutputField webhookResponse =
|
||||
let mkResponseObject :: IR.ActionFields -> HashMap Text J.Value -> AO.Value
|
||||
mkResponseObject fields obj =
|
||||
AO.object $
|
||||
@ -191,13 +191,27 @@ makeActionResponseNoRelations annFields webhookResponse =
|
||||
in -- NOTE (Sam): This case would still not allow for aliased fields to be
|
||||
-- a part of the response. Also, seeing that none of the other `annField`
|
||||
-- types would be caught in the example, I've chosen to leave it as it is.
|
||||
case webhookResponse of
|
||||
AWRArray objs -> AO.array $ map mkResponseArray objs
|
||||
AWRObject obj -> mkResponseObject annFields (mapKeys G.unName obj)
|
||||
AWRNum n -> AO.toOrdered n
|
||||
AWRBool b -> AO.toOrdered b
|
||||
AWRString s -> AO.toOrdered s
|
||||
AWRNull -> AO.Null
|
||||
-- NOTE: (Pranshi) Bool here is applied to specify if we want to check ActionOutputFields
|
||||
-- (in async actions, we have object types, which need to be validated
|
||||
-- and ActionOutputField information is not present in `resolveAsyncActionQuery` -
|
||||
-- so added a boolean which will make sure that the response is validated)
|
||||
if gTypeContains isCustomScalar (unGraphQLType outputType) outputF && shouldCheckOutputField
|
||||
then AO.toOrdered $ J.toJSON webhookResponse
|
||||
else case webhookResponse of
|
||||
AWRArray objs -> AO.array $ map mkResponseArray objs
|
||||
AWRObject obj ->
|
||||
mkResponseObject annFields (mapKeys G.unName obj)
|
||||
AWRNull -> AO.Null
|
||||
_ -> AO.toOrdered $ J.toJSON webhookResponse
|
||||
|
||||
gTypeContains :: (G.GType -> IR.ActionOutputFields -> Bool) -> G.GType -> IR.ActionOutputFields -> Bool
|
||||
gTypeContains fun gType aof = case gType of
|
||||
t@(G.TypeNamed _ _) -> fun t aof
|
||||
(G.TypeList _ expectedType) -> gTypeContains fun expectedType aof
|
||||
|
||||
isCustomScalar :: G.GType -> IR.ActionOutputFields -> Bool
|
||||
isCustomScalar (G.TypeNamed _ name) outputF = isJust (lookup (G.unName name) pgScalarTranslations) || (Map.null outputF && (not (isInBuiltScalar (G.unName name))))
|
||||
isCustomScalar (G.TypeList _ _) _ = False
|
||||
|
||||
{- Note: [Async action architecture]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@ -266,7 +280,7 @@ resolveAsyncActionQuery userInfo annAction =
|
||||
IR.AsyncOutput annFields ->
|
||||
fromMaybe AO.Null <$> forM
|
||||
_alrResponsePayload
|
||||
\response -> makeActionResponseNoRelations annFields <$> decodeValue response
|
||||
\response -> makeActionResponseNoRelations annFields outputType Map.empty False <$> decodeValue response
|
||||
IR.AsyncId -> pure $ AO.String $ actionIdToText actionId
|
||||
IR.AsyncCreatedAt -> pure $ AO.toOrdered $ J.toJSON _alrCreatedAt
|
||||
IR.AsyncErrors -> pure $ AO.toOrdered $ J.toJSON _alrErrors
|
||||
@ -570,7 +584,7 @@ callWebhook
|
||||
| HTTP.statusIsSuccessful responseStatus -> do
|
||||
modifyQErr addInternalToErr $ do
|
||||
webhookResponse <- decodeValue responseValue
|
||||
validateResponse responseValue outputType
|
||||
validateResponse responseValue outputType outputFields
|
||||
pure (webhookResponse, mkSetCookieHeaders responseWreq)
|
||||
| HTTP.statusIsClientError responseStatus -> do
|
||||
ActionWebhookErrorResponse message maybeCode maybeExtensions <-
|
||||
@ -604,44 +618,37 @@ callWebhook
|
||||
throwUnexpected $
|
||||
"expecting not null value for field " <>> fieldName
|
||||
|
||||
isScalar :: Text -> Bool
|
||||
isScalar s
|
||||
| s == "Int" = True
|
||||
| s == "Float" = True
|
||||
| s == "String" = True
|
||||
| s == "Boolean" = True
|
||||
| otherwise = False
|
||||
|
||||
-- Validates the webhook response against the output type
|
||||
validateResponse :: J.Value -> GraphQLType -> m ()
|
||||
validateResponse webhookResponse' outputType' =
|
||||
case (webhookResponse', outputType') of
|
||||
(J.Null, _) -> unless (isNullableType outputType') $ throwUnexpected "got null for the action webhook response"
|
||||
(J.Number _, (GraphQLType (G.TypeNamed _ name))) -> do
|
||||
unless (G.unName name == "Int" || G.unName name == "Float") $
|
||||
throwUnexpected $ "got scalar Number for the action webhook response, expecting " <> G.unName name
|
||||
(J.Bool _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
unless (G.unName name == "Boolean") $
|
||||
throwUnexpected $ "got scalar Boolean for the action webhook response, expecting " <> G.unName name
|
||||
(J.String _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
unless (G.unName name == "String") $
|
||||
throwUnexpected $ "got scalar String for the action webhook response, expecting " <> G.unName name
|
||||
(J.Array _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
throwUnexpected $ "got array for the action webhook response, expecting " <> G.unName name
|
||||
(J.Array objs, (GraphQLType (G.TypeList _ outputType''))) ->
|
||||
traverse_ (\o -> validateResponse o (GraphQLType outputType'')) objs
|
||||
(ob@(J.Object _), (GraphQLType (G.TypeNamed _ name))) -> do
|
||||
case J.parse (fmap AWRObject . J.parseJSON) ob of
|
||||
J.Error s -> throwUnexpected $ "failed to parse webhookResponse as Object, error: " <> T.pack s -- This should never happen
|
||||
J.Success (AWRObject awr) -> do
|
||||
when (isScalar (G.unName name)) $
|
||||
throwUnexpected $ "got object for the action webhook response, expecting " <> G.unName name
|
||||
validateResponseObject awr
|
||||
J.Success _ ->
|
||||
throwUnexpected "Webhook response not an object" -- This should never happen
|
||||
-- Expected output type is an array and webhook response is not an array
|
||||
(_, (GraphQLType (G.TypeList _ _))) ->
|
||||
throwUnexpected $ "expecting array for the action webhook response"
|
||||
validateResponse :: J.Value -> GraphQLType -> IR.ActionOutputFields -> m ()
|
||||
validateResponse webhookResponse' outputType' outputF =
|
||||
unless (isCustomScalar (unGraphQLType outputType') outputF) do
|
||||
case (webhookResponse', outputType') of
|
||||
(J.Null, _) -> unless (isNullableType outputType') $ throwUnexpected "got null for the action webhook response"
|
||||
(J.Number _, (GraphQLType (G.TypeNamed _ name))) -> do
|
||||
unless (G.unName name == G.unName intScalar || G.unName name == G.unName floatScalar) $
|
||||
throwUnexpected $ "got scalar Number for the action webhook response, expecting " <> G.unName name
|
||||
(J.Bool _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
unless (G.unName name == G.unName boolScalar) $
|
||||
throwUnexpected $ "got scalar Boolean for the action webhook response, expecting " <> G.unName name
|
||||
(J.String _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
unless (G.unName name == G.unName stringScalar || G.unName name == G.unName idScalar) $
|
||||
throwUnexpected $ "got scalar String for the action webhook response, expecting " <> G.unName name
|
||||
(J.Array _, (GraphQLType (G.TypeNamed _ name))) ->
|
||||
throwUnexpected $ "got array for the action webhook response, expecting " <> G.unName name
|
||||
(J.Array objs, (GraphQLType (G.TypeList _ outputType''))) ->
|
||||
traverse_ (\o -> validateResponse o (GraphQLType outputType'') outputF) objs
|
||||
(ob@(J.Object _), (GraphQLType (G.TypeNamed _ name))) -> do
|
||||
case J.parse (fmap AWRObject . J.parseJSON) ob of
|
||||
J.Error s -> throwUnexpected $ "failed to parse webhookResponse as Object, error: " <> T.pack s -- This should never happen
|
||||
J.Success (AWRObject awr) -> do
|
||||
when (isInBuiltScalar (G.unName name)) $
|
||||
throwUnexpected $ "got object for the action webhook response, expecting " <> G.unName name
|
||||
validateResponseObject awr
|
||||
J.Success _ ->
|
||||
throwUnexpected "Webhook response not an object" -- This should never happen
|
||||
-- Expected output type is an array and webhook response is not an array
|
||||
(_, (GraphQLType (G.TypeList _ _))) ->
|
||||
throwUnexpected $ "expecting array for the action webhook response"
|
||||
|
||||
processOutputSelectionSet ::
|
||||
TF.ArgumentExp v ->
|
||||
|
@ -147,8 +147,10 @@ resolveAction env AnnotatedCustomTypes {..} ActionDefinition {..} allScalars = d
|
||||
pure aoTScalar
|
||||
| Just objectType <- Map.lookup outputBaseType _actObjects ->
|
||||
pure $ AOTObject objectType
|
||||
| Just (NOCTScalar s) <- Map.lookup outputBaseType _actNonObjects -> do
|
||||
pure (AOTScalar s)
|
||||
| otherwise ->
|
||||
throw400 NotExists ("the type: " <> dquote outputBaseType <> " is not an object type defined in custom types")
|
||||
throw400 NotExists ("the type: " <> dquote outputBaseType <> " is not an object or scalar type defined in custom types")
|
||||
-- If the Action is sync:
|
||||
-- 1. Check if the output type has only top level relations (if any)
|
||||
-- If the Action is async:
|
||||
|
@ -6,6 +6,7 @@ module Hasura.RQL.Types.CustomTypes
|
||||
GraphQLType (..),
|
||||
isListType,
|
||||
isNullableType,
|
||||
isInBuiltScalar,
|
||||
EnumTypeName (..),
|
||||
EnumValueDefinition (..),
|
||||
EnumTypeDefinition (..),
|
||||
@ -86,6 +87,15 @@ isListType (GraphQLType ty) = G.isListType ty
|
||||
isNullableType :: GraphQLType -> Bool
|
||||
isNullableType (GraphQLType ty) = G.isNullable ty
|
||||
|
||||
isInBuiltScalar :: Text -> Bool
|
||||
isInBuiltScalar s
|
||||
| s == G.unName intScalar = True
|
||||
| s == G.unName floatScalar = True
|
||||
| s == G.unName stringScalar = True
|
||||
| s == G.unName boolScalar = True
|
||||
| s == G.unName idScalar = True
|
||||
| otherwise = False
|
||||
|
||||
newtype InputObjectFieldName = InputObjectFieldName {unInputObjectFieldName :: G.Name}
|
||||
deriving (Show, Eq, Ord, Hashable, J.FromJSON, J.ToJSON, ToTxt, Generic, NFData, Cacheable)
|
||||
|
||||
|
@ -372,7 +372,15 @@ class ActionsWebhookHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
elif req_path == "/scalar-response":
|
||||
self._send_response(HTTPStatus.OK, "some-string")
|
||||
|
||||
elif req_path == "/json-response":
|
||||
resp, status = self.json_response()
|
||||
self._send_response(status, resp)
|
||||
|
||||
elif req_path == "/custom-scalar-array-response":
|
||||
resp, status = self.custom_scalar_array_response()
|
||||
self._send_response(status, resp)
|
||||
|
||||
elif req_path == "/scalar-array-response":
|
||||
self._send_response(HTTPStatus.OK, ["foo", "bar", None])
|
||||
|
||||
@ -602,6 +610,18 @@ class ActionsWebhookHandler(http.server.BaseHTTPRequestHandler):
|
||||
def null_response(self):
|
||||
response = None
|
||||
return response, HTTPStatus.OK
|
||||
|
||||
def json_response(self):
|
||||
response = {
|
||||
'foo': 'bar'
|
||||
}
|
||||
return response, HTTPStatus.OK
|
||||
|
||||
def custom_scalar_array_response(self):
|
||||
response = [{
|
||||
'foo': 'bar'
|
||||
}]
|
||||
return response, HTTPStatus.OK
|
||||
|
||||
def recursive_output(self):
|
||||
return {
|
||||
|
@ -0,0 +1,12 @@
|
||||
description: custom scalar array response should be allowed
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
custom_scalar_array_response
|
||||
}
|
||||
response:
|
||||
data:
|
||||
custom_scalar_array_response:
|
||||
[foo: bar]
|
@ -0,0 +1,12 @@
|
||||
description: custom scalar response should be allowed
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
custom_scalar_response
|
||||
}
|
||||
response:
|
||||
data:
|
||||
custom_scalar_response:
|
||||
foo: bar
|
@ -0,0 +1,12 @@
|
||||
description: pgScalar (custom scalar) response should be allowed
|
||||
url: /v1/graphql
|
||||
status: 200
|
||||
query:
|
||||
query: |
|
||||
mutation {
|
||||
pgscalar_response
|
||||
}
|
||||
response:
|
||||
data:
|
||||
pgscalar_response:
|
||||
foo: bar
|
@ -60,6 +60,9 @@ args:
|
||||
type: String
|
||||
- name: extensions
|
||||
type: json
|
||||
|
||||
scalars:
|
||||
- name: myCustomScalar
|
||||
|
||||
objects:
|
||||
- name: UserId
|
||||
@ -442,6 +445,24 @@ args:
|
||||
output_type: String!
|
||||
handler: http://127.0.0.1:5593/scalar-response
|
||||
|
||||
- type: create_action
|
||||
args:
|
||||
name: pgscalar_response
|
||||
definition:
|
||||
kind: synchronous
|
||||
arguments:
|
||||
output_type: json!
|
||||
handler: http://127.0.0.1:5593/json-response
|
||||
|
||||
- type: create_action
|
||||
args:
|
||||
name: custom_scalar_response
|
||||
definition:
|
||||
kind: synchronous
|
||||
arguments:
|
||||
output_type: myCustomScalar!
|
||||
handler: http://127.0.0.1:5593/json-response
|
||||
|
||||
- type: create_action
|
||||
args:
|
||||
name: scalar_array_response
|
||||
@ -451,6 +472,24 @@ args:
|
||||
output_type: '[String]!'
|
||||
handler: http://127.0.0.1:5593/scalar-array-response
|
||||
|
||||
- type: create_action
|
||||
args:
|
||||
name: custom_scalar_array_response
|
||||
definition:
|
||||
kind: synchronous
|
||||
arguments:
|
||||
output_type: '[myCustomScalar!]!'
|
||||
handler: http://127.0.0.1:5593/custom-scalar-array-response
|
||||
|
||||
- type: create_action
|
||||
args:
|
||||
name: custom_scalar_nested_array_response
|
||||
definition:
|
||||
kind: synchronous
|
||||
arguments:
|
||||
output_type: '[[myCustomScalar!]]!'
|
||||
handler: http://127.0.0.1:5593/custom-scalar-array-response
|
||||
|
||||
- type: create_select_permission
|
||||
args:
|
||||
table: user
|
||||
|
@ -60,10 +60,26 @@ args:
|
||||
args:
|
||||
name: scalar_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: pgscalar_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: custom_scalar_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: scalar_array_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: custom_scalar_array_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: custom_scalar_nested_array_response
|
||||
clear_data: true
|
||||
- type: drop_action
|
||||
args:
|
||||
name: recursive_output
|
||||
|
@ -96,7 +96,7 @@ class TestActionsSync:
|
||||
|
||||
def test_expecting_scalar_string_output_type_got_object(self, hge_ctx):
|
||||
check_query_secret(hge_ctx, self.dir() + '/expecting_scalar_response_got_object.yaml')
|
||||
|
||||
|
||||
def test_expecting_object_output_type_got_scalar_string(self, hge_ctx):
|
||||
check_query_secret(hge_ctx, self.dir() + '/expecting_object_response_got_scalar.yaml')
|
||||
|
||||
@ -118,6 +118,34 @@ class TestActionsSync:
|
||||
|
||||
def test_expecting_object_response_with_nested_null(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/expecting_object_response_with_nested_null.yaml')
|
||||
|
||||
def test_expecting_jsonb_response_success(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/expecting_jsonb_response_success.yaml')
|
||||
|
||||
def test_expecting_custom_scalar_response_success(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/expecting_custom_scalar_response_success.yaml')
|
||||
|
||||
def test_expecting_custom_scalar_array_response_success(self, hge_ctx):
|
||||
check_query_f(hge_ctx, self.dir() + '/expecting_custom_scalar_array_response_success.yaml')
|
||||
|
||||
def test_expecting_custom_scalar_array_response_got_different_type(self, hge_ctx):
|
||||
query_obj = {
|
||||
"query": """
|
||||
mutation {
|
||||
custom_scalar_nested_array_response
|
||||
}
|
||||
"""
|
||||
}
|
||||
headers = {}
|
||||
admin_secret = hge_ctx.hge_key
|
||||
if admin_secret is not None:
|
||||
headers['X-Hasura-Admin-Secret'] = admin_secret
|
||||
code, resp, _ = hge_ctx.anyq('/v1/graphql', query_obj, headers)
|
||||
assert code == 200, resp
|
||||
|
||||
error_message = resp['errors'][0]['message']
|
||||
|
||||
assert error_message == 'expecting array for the action webhook response', error_message
|
||||
|
||||
def test_expecting_object_response_with_nested_null_wrong_field(self, hge_ctx):
|
||||
query_obj = {
|
||||
|
Loading…
Reference in New Issue
Block a user