mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 12:31:52 +03:00
b75706dd12
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2338 Co-authored-by: Sameer Kolhar <6604943+kolharsam@users.noreply.github.com> GitOrigin-RevId: 9cd6bb940ba74fe41bdf2db3c4f35e89c4fb0577
578 lines
24 KiB
Haskell
578 lines
24 KiB
Haskell
module Hasura.GraphQL.RemoteServer
|
|
( fetchRemoteSchema,
|
|
IntrospectionResult,
|
|
parseIntrospectionResult,
|
|
execRemoteGQ,
|
|
identityCustomizer,
|
|
introspectionQuery,
|
|
-- The following exports are needed for unit tests
|
|
getCustomizer,
|
|
validateSchemaCustomizationsDistinct,
|
|
getSchemaIntrospection,
|
|
)
|
|
where
|
|
|
|
import Control.Arrow.Extended (left)
|
|
import Control.Exception (try)
|
|
import Control.Exception.Safe (Exception (displayException), Typeable, impureThrow)
|
|
import Control.Lens (set, (^.))
|
|
import Data.Aeson ((.:), (.:?))
|
|
import Data.Aeson qualified as J
|
|
import Data.Aeson.Ordered qualified as JO
|
|
import Data.Aeson.Types qualified as J
|
|
import Data.ByteString.Lazy qualified as BL
|
|
import Data.Environment qualified as Env
|
|
import Data.FileEmbed (makeRelativeToProject)
|
|
import Data.HashMap.Strict.Extended qualified as Map
|
|
import Data.HashMap.Strict.InsOrd qualified as OMap
|
|
import Data.HashSet qualified as Set
|
|
import Data.List.Extended (duplicates)
|
|
import Data.Text qualified as T
|
|
import Data.Text.Extended (dquoteList, (<<>))
|
|
import Hasura.Base.Error
|
|
import Hasura.GraphQL.Context
|
|
import Hasura.GraphQL.Namespace (mkUnNamespacedRootFieldAlias)
|
|
import Hasura.GraphQL.Parser.Collect ()
|
|
import Hasura.GraphQL.Parser.Schema (Variable)
|
|
-- Needed for GHCi and HLS due to TH in cyclically dependent modules (see https://gitlab.haskell.org/ghc/ghc/-/issues/1012)
|
|
import Hasura.GraphQL.Schema.Remote (buildRemoteParser)
|
|
import Hasura.GraphQL.Transport.HTTP.Protocol
|
|
import Hasura.HTTP
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.DDL.Headers (makeHeadersFromConf)
|
|
import Hasura.RQL.IR (RootField (RFRaw))
|
|
import Hasura.RQL.Types
|
|
import Hasura.Server.Utils
|
|
import Hasura.Session
|
|
import Hasura.Tracing qualified as Tracing
|
|
import Language.GraphQL.Draft.Parser qualified as G
|
|
import Language.GraphQL.Draft.Syntax qualified as G
|
|
import Language.Haskell.TH.Syntax qualified as TH
|
|
import Network.HTTP.Client.Transformable qualified as HTTP
|
|
import Network.URI (URI)
|
|
import Network.Wreq qualified as Wreq
|
|
|
|
introspectionQuery :: GQLReqOutgoing
|
|
introspectionQuery =
|
|
$( do
|
|
fp <- makeRelativeToProject "src-rsr/introspection.json"
|
|
TH.qAddDependentFile fp
|
|
eitherResult <- TH.runIO $ J.eitherDecodeFileStrict fp
|
|
either fail TH.lift $ do
|
|
r@GQLReq {..} <- eitherResult
|
|
op <- left show $ getSingleOperation r
|
|
pure GQLReq {_grQuery = op, ..}
|
|
)
|
|
|
|
validateSchemaCustomizations ::
|
|
forall m.
|
|
MonadError QErr m =>
|
|
RemoteSchemaCustomizer ->
|
|
RemoteSchemaIntrospection ->
|
|
m ()
|
|
validateSchemaCustomizations remoteSchemaCustomizer remoteSchemaIntrospection = do
|
|
validateSchemaCustomizationsConsistent remoteSchemaCustomizer remoteSchemaIntrospection
|
|
validateSchemaCustomizationsDistinct remoteSchemaCustomizer remoteSchemaIntrospection
|
|
|
|
validateSchemaCustomizationsConsistent ::
|
|
forall m.
|
|
MonadError QErr m =>
|
|
RemoteSchemaCustomizer ->
|
|
RemoteSchemaIntrospection ->
|
|
m ()
|
|
validateSchemaCustomizationsConsistent remoteSchemaCustomizer (RemoteSchemaIntrospection typeDefinitions) = do
|
|
traverse_ validateInterfaceFields typeDefinitions
|
|
where
|
|
customizeFieldName = remoteSchemaCustomizeFieldName remoteSchemaCustomizer
|
|
|
|
validateInterfaceFields :: G.TypeDefinition [G.Name] a -> m ()
|
|
validateInterfaceFields = \case
|
|
G.TypeDefinitionInterface G.InterfaceTypeDefinition {..} ->
|
|
for_ _itdPossibleTypes $ \typeName ->
|
|
for_ _itdFieldsDefinition $ \G.FieldDefinition {..} -> do
|
|
let interfaceCustomizedFieldName = runCustomizeRemoteFieldName customizeFieldName _itdName _fldName
|
|
typeCustomizedFieldName = runCustomizeRemoteFieldName customizeFieldName typeName _fldName
|
|
when (interfaceCustomizedFieldName /= typeCustomizedFieldName) $
|
|
throwRemoteSchema $
|
|
"Remote schema customization inconsistency: field name mapping for field "
|
|
<> _fldName
|
|
<<> " of interface "
|
|
<> _itdName
|
|
<<> " is inconsistent with mapping for type "
|
|
<> typeName
|
|
<<> ". Interface field name maps to "
|
|
<> interfaceCustomizedFieldName
|
|
<<> ". Type field name maps to "
|
|
<> typeCustomizedFieldName
|
|
<<> "."
|
|
_ -> pure ()
|
|
|
|
validateSchemaCustomizationsDistinct ::
|
|
forall m.
|
|
MonadError QErr m =>
|
|
RemoteSchemaCustomizer ->
|
|
RemoteSchemaIntrospection ->
|
|
m ()
|
|
validateSchemaCustomizationsDistinct remoteSchemaCustomizer (RemoteSchemaIntrospection typeDefinitions) = do
|
|
validateTypeMappingsAreDistinct
|
|
traverse_ validateFieldMappingsAreDistinct typeDefinitions
|
|
where
|
|
customizeTypeName = remoteSchemaCustomizeTypeName remoteSchemaCustomizer
|
|
customizeFieldName = runCustomizeRemoteFieldName (remoteSchemaCustomizeFieldName remoteSchemaCustomizer)
|
|
|
|
validateTypeMappingsAreDistinct :: m ()
|
|
validateTypeMappingsAreDistinct = do
|
|
let dups = duplicates $ runMkTypename customizeTypeName <$> Map.keys typeDefinitions
|
|
unless (Set.null dups) $
|
|
throwRemoteSchema $
|
|
"Type name mappings are not distinct; the following types appear more than once: "
|
|
<> dquoteList dups
|
|
|
|
validateFieldMappingsAreDistinct :: G.TypeDefinition a b -> m ()
|
|
validateFieldMappingsAreDistinct = \case
|
|
G.TypeDefinitionInterface G.InterfaceTypeDefinition {..} -> do
|
|
let dups = duplicates $ (customizeFieldName _itdName . G._fldName) <$> _itdFieldsDefinition
|
|
unless (Set.null dups) $
|
|
throwRemoteSchema $
|
|
"Field name mappings for interface type " <> _itdName
|
|
<<> " are not distinct; the following fields appear more than once: "
|
|
<> dquoteList dups
|
|
G.TypeDefinitionObject G.ObjectTypeDefinition {..} -> do
|
|
let dups = duplicates $ (customizeFieldName _otdName . G._fldName) <$> _otdFieldsDefinition
|
|
unless (Set.null dups) $
|
|
throwRemoteSchema $
|
|
"Field name mappings for object type " <> _otdName
|
|
<<> " are not distinct; the following fields appear more than once: "
|
|
<> dquoteList dups
|
|
_ -> pure ()
|
|
|
|
parseIntrospectionResult :: J.Value -> Maybe IntrospectionResult
|
|
parseIntrospectionResult = fmap fromIntrospection . J.parseMaybe J.parseJSON
|
|
|
|
-- | Make an introspection query to the remote graphql server for the data we
|
|
-- need to present and stitch the remote schema. This powers add_remote_schema,
|
|
-- and also is called by schema cache rebuilding code in "Hasura.RQL.DDL.Schema.Cache".
|
|
fetchRemoteSchema ::
|
|
forall m.
|
|
(MonadIO m, MonadError QErr m, Tracing.MonadTrace m) =>
|
|
Env.Environment ->
|
|
HTTP.Manager ->
|
|
RemoteSchemaName ->
|
|
ValidatedRemoteSchemaDef ->
|
|
m RemoteSchemaCtx
|
|
fetchRemoteSchema env manager _rscName rsDef@ValidatedRemoteSchemaDef {..} = do
|
|
(_, _, _rscRawIntrospectionResult) <-
|
|
execRemoteGQ env manager adminUserInfo [] rsDef introspectionQuery
|
|
|
|
-- Parse the JSON into flat GraphQL type AST
|
|
FromIntrospection _rscIntroOriginal <-
|
|
J.eitherDecode _rscRawIntrospectionResult `onLeft` (throwRemoteSchema . T.pack)
|
|
|
|
-- possibly transform type names from the remote schema, per the user's 'RemoteSchemaDef'
|
|
let rsCustomizer = getCustomizer (addDefaultRoots _rscIntroOriginal) _vrsdCustomization
|
|
|
|
validateSchemaCustomizations rsCustomizer (irDoc _rscIntroOriginal)
|
|
|
|
let _rscInfo = RemoteSchemaInfo {..}
|
|
|
|
-- At this point, we can't resolve remote relationships; we store an empty map.
|
|
let _rscRemoteRelationships = mempty
|
|
|
|
-- Check that the parsed GraphQL type info is valid by running the schema generation
|
|
_rscParsed <- buildRemoteParser _rscIntroOriginal _rscInfo
|
|
|
|
-- The 'rawIntrospectionResult' contains the 'Bytestring' response of
|
|
-- the introspection result of the remote server. We store this in the
|
|
-- 'RemoteSchemaCtx' because we can use this when the 'introspect_remote_schema'
|
|
-- is called by simple encoding the result to JSON.
|
|
return
|
|
RemoteSchemaCtx
|
|
{ _rscPermissions = mempty,
|
|
..
|
|
}
|
|
where
|
|
-- If there is no explicit mutation or subscription root type we need to check for
|
|
-- objects type definitions with the default names "Mutation" and "Subscription".
|
|
-- If found, we add the default roots explicitly to the IntrospectionResult.
|
|
-- This simplifies the customization code.
|
|
addDefaultRoots :: IntrospectionResult -> IntrospectionResult
|
|
addDefaultRoots IntrospectionResult {..} =
|
|
IntrospectionResult
|
|
{ irMutationRoot = getRootTypeName $$(G.litName "Mutation") irMutationRoot,
|
|
irSubscriptionRoot = getRootTypeName $$(G.litName "Subscription") irSubscriptionRoot,
|
|
..
|
|
}
|
|
where
|
|
getRootTypeName defaultName providedName =
|
|
providedName <|> (defaultName <$ lookupObject irDoc defaultName)
|
|
|
|
-- | Parsing the introspection query result. We use this newtype wrapper to
|
|
-- avoid orphan instances and parse JSON in the way that we need for GraphQL
|
|
-- introspection results.
|
|
newtype FromIntrospection a = FromIntrospection {fromIntrospection :: a}
|
|
deriving (Show, Eq, Generic, Functor)
|
|
|
|
pErr :: (MonadFail m) => Text -> m a
|
|
pErr = fail . T.unpack
|
|
|
|
kindErr :: (MonadFail m) => Text -> Text -> m a
|
|
kindErr gKind eKind = pErr $ "Invalid `kind: " <> gKind <> "` in " <> eKind
|
|
|
|
instance J.FromJSON (FromIntrospection G.Description) where
|
|
parseJSON = fmap (FromIntrospection . G.Description) . J.parseJSON
|
|
|
|
instance J.FromJSON (FromIntrospection G.ScalarTypeDefinition) where
|
|
parseJSON = J.withObject "ScalarTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
when (kind /= "SCALAR") $ kindErr kind "scalar"
|
|
let desc' = fmap fromIntrospection desc
|
|
r = G.ScalarTypeDefinition desc' name []
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection (G.ObjectTypeDefinition G.InputValueDefinition)) where
|
|
parseJSON = J.withObject "ObjectTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
fields <- o .:? "fields"
|
|
interfaces :: Maybe [FromIntrospection (G.InterfaceTypeDefinition [G.Name] G.InputValueDefinition)] <- o .:? "interfaces"
|
|
when (kind /= "OBJECT") $ kindErr kind "object"
|
|
let implIfaces = map G._itdName $ maybe [] (fmap fromIntrospection) interfaces
|
|
flds = maybe [] (fmap fromIntrospection) fields
|
|
desc' = fmap fromIntrospection desc
|
|
r = G.ObjectTypeDefinition desc' name implIfaces [] flds
|
|
return $ FromIntrospection r
|
|
|
|
instance (J.FromJSON (FromIntrospection a)) => J.FromJSON (FromIntrospection (G.FieldDefinition a)) where
|
|
parseJSON = J.withObject "FieldDefinition" $ \o -> do
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
args <- o .: "args"
|
|
_type <- o .: "type"
|
|
let desc' = fmap fromIntrospection desc
|
|
r =
|
|
G.FieldDefinition
|
|
desc'
|
|
name
|
|
(fmap fromIntrospection args)
|
|
(fromIntrospection _type)
|
|
[]
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection G.GType) where
|
|
parseJSON = J.withObject "GType" $ \o -> do
|
|
kind <- o .: "kind"
|
|
mName <- o .:? "name"
|
|
mType <- o .:? "ofType"
|
|
r <- case (kind, mName, mType) of
|
|
("NON_NULL", _, Just typ) -> return $ mkNotNull (fromIntrospection typ)
|
|
("NON_NULL", _, Nothing) -> pErr "NON_NULL should have `ofType`"
|
|
("LIST", _, Just typ) ->
|
|
return $ G.TypeList (G.Nullability True) (fromIntrospection typ)
|
|
("LIST", _, Nothing) -> pErr "LIST should have `ofType`"
|
|
(_, Just name, _) -> return $ G.TypeNamed (G.Nullability True) name
|
|
_ -> pErr $ "kind: " <> kind <> " should have name"
|
|
return $ FromIntrospection r
|
|
where
|
|
mkNotNull typ = case typ of
|
|
G.TypeList _ ty -> G.TypeList (G.Nullability False) ty
|
|
G.TypeNamed _ n -> G.TypeNamed (G.Nullability False) n
|
|
|
|
instance J.FromJSON (FromIntrospection G.InputValueDefinition) where
|
|
parseJSON = J.withObject "InputValueDefinition" $ \o -> do
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
_type <- o .: "type"
|
|
defVal <- o .:? "defaultValue"
|
|
let desc' = fmap fromIntrospection desc
|
|
let defVal' = fmap fromIntrospection defVal
|
|
r = G.InputValueDefinition desc' name (fromIntrospection _type) defVal' []
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection (G.Value Void)) where
|
|
parseJSON = J.withText "Value Void" $ \t ->
|
|
let parseValueConst = G.runParser G.value
|
|
in FromIntrospection <$> onLeft (parseValueConst t) (fail . T.unpack)
|
|
|
|
instance J.FromJSON (FromIntrospection (G.InterfaceTypeDefinition [G.Name] G.InputValueDefinition)) where
|
|
parseJSON = J.withObject "InterfaceTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
fields <- o .:? "fields"
|
|
possibleTypes :: Maybe [FromIntrospection (G.ObjectTypeDefinition G.InputValueDefinition)] <- o .:? "possibleTypes"
|
|
let flds = maybe [] (fmap fromIntrospection) fields
|
|
desc' = fmap fromIntrospection desc
|
|
possTps = map G._otdName $ maybe [] (fmap fromIntrospection) possibleTypes
|
|
when (kind /= "INTERFACE") $ kindErr kind "interface"
|
|
-- TODO (non PDV) track which interfaces implement which other interfaces, after a
|
|
-- GraphQL spec > Jun 2018 is released.
|
|
let r = G.InterfaceTypeDefinition desc' name [] flds possTps
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection G.UnionTypeDefinition) where
|
|
parseJSON = J.withObject "UnionTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
possibleTypes :: [FromIntrospection (G.ObjectTypeDefinition G.InputValueDefinition)] <- o .: "possibleTypes"
|
|
let possibleTypes' = map G._otdName $ fmap fromIntrospection possibleTypes
|
|
desc' = fmap fromIntrospection desc
|
|
when (kind /= "UNION") $ kindErr kind "union"
|
|
let r = G.UnionTypeDefinition desc' name [] possibleTypes'
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection G.EnumTypeDefinition) where
|
|
parseJSON = J.withObject "EnumTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
vals <- o .: "enumValues"
|
|
when (kind /= "ENUM") $ kindErr kind "enum"
|
|
let desc' = fmap fromIntrospection desc
|
|
let r = G.EnumTypeDefinition desc' name [] (fmap fromIntrospection vals)
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection G.EnumValueDefinition) where
|
|
parseJSON = J.withObject "EnumValueDefinition" $ \o -> do
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
let desc' = fmap fromIntrospection desc
|
|
let r = G.EnumValueDefinition desc' name []
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection (G.InputObjectTypeDefinition G.InputValueDefinition)) where
|
|
parseJSON = J.withObject "InputObjectTypeDefinition" $ \o -> do
|
|
kind <- o .: "kind"
|
|
name <- o .: "name"
|
|
desc <- o .:? "description"
|
|
mInputFields <- o .:? "inputFields"
|
|
let inputFields = maybe [] (fmap fromIntrospection) mInputFields
|
|
let desc' = fmap fromIntrospection desc
|
|
when (kind /= "INPUT_OBJECT") $ kindErr kind "input_object"
|
|
let r = G.InputObjectTypeDefinition desc' name [] inputFields
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection (G.TypeDefinition [G.Name] G.InputValueDefinition)) where
|
|
parseJSON = J.withObject "TypeDefinition" $ \o -> do
|
|
kind :: Text <- o .: "kind"
|
|
r <- case kind of
|
|
"SCALAR" ->
|
|
G.TypeDefinitionScalar . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
"OBJECT" ->
|
|
G.TypeDefinitionObject . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
"INTERFACE" ->
|
|
G.TypeDefinitionInterface . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
"UNION" ->
|
|
G.TypeDefinitionUnion . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
"ENUM" ->
|
|
G.TypeDefinitionEnum . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
"INPUT_OBJECT" ->
|
|
G.TypeDefinitionInputObject . fromIntrospection <$> J.parseJSON (J.Object o)
|
|
_ -> pErr $ "unknown kind: " <> kind
|
|
return $ FromIntrospection r
|
|
|
|
instance J.FromJSON (FromIntrospection IntrospectionResult) where
|
|
parseJSON = J.withObject "SchemaDocument" $ \o -> do
|
|
_data <- o .: "data"
|
|
schema <- _data .: "__schema"
|
|
-- the list of types
|
|
types <- schema .: "types"
|
|
-- query root
|
|
queryType <- schema .: "queryType"
|
|
queryRoot <- queryType .: "name"
|
|
-- mutation root
|
|
mMutationType <- schema .:? "mutationType"
|
|
mutationRoot <- case mMutationType of
|
|
Nothing -> return Nothing
|
|
Just mutType -> do
|
|
mutRoot <- mutType .: "name"
|
|
return $ Just mutRoot
|
|
-- subscription root
|
|
mSubsType <- schema .:? "subscriptionType"
|
|
subsRoot <- case mSubsType of
|
|
Nothing -> return Nothing
|
|
Just subsType -> do
|
|
subRoot <- subsType .: "name"
|
|
return $ Just subRoot
|
|
let types' =
|
|
(fmap . fmap . fmap)
|
|
-- presets are only defined for non-admin roles,
|
|
-- an admin will not have any presets
|
|
-- defined and the admin will be the one,
|
|
-- who'll be adding the remote schema,
|
|
-- hence presets are set to `Nothing`
|
|
(`RemoteSchemaInputValueDefinition` Nothing)
|
|
types
|
|
r =
|
|
IntrospectionResult
|
|
(RemoteSchemaIntrospection $ Map.fromListOn getTypeName $ fromIntrospection <$> types')
|
|
queryRoot
|
|
mutationRoot
|
|
subsRoot
|
|
return $ FromIntrospection r
|
|
|
|
objectWithoutNullValues :: [J.Pair] -> J.Value
|
|
objectWithoutNullValues = J.object . filter notNull
|
|
where
|
|
notNull (_, J.Null) = False
|
|
notNull _ = True
|
|
|
|
toObjectTypeDefinition :: G.Name -> G.ObjectTypeDefinition G.InputValueDefinition
|
|
toObjectTypeDefinition name = G.ObjectTypeDefinition Nothing name [] [] []
|
|
|
|
execRemoteGQ ::
|
|
( MonadIO m,
|
|
MonadError QErr m,
|
|
Tracing.MonadTrace m
|
|
) =>
|
|
Env.Environment ->
|
|
HTTP.Manager ->
|
|
UserInfo ->
|
|
[HTTP.Header] ->
|
|
ValidatedRemoteSchemaDef ->
|
|
GQLReqOutgoing ->
|
|
-- | Returns the response body and headers, along with the time taken for the
|
|
-- HTTP request to complete
|
|
m (DiffTime, [HTTP.Header], BL.ByteString)
|
|
execRemoteGQ env manager userInfo reqHdrs rsdef gqlReq@GQLReq {..} = do
|
|
let gqlReqUnparsed = renderGQLReqOutgoing gqlReq
|
|
|
|
when (G._todType _grQuery == G.OperationTypeSubscription) $
|
|
throwRemoteSchema "subscription to remote server is not supported"
|
|
confHdrs <- makeHeadersFromConf env hdrConf
|
|
let clientHdrs = bool [] (mkClientHeadersForward reqHdrs) fwdClientHdrs
|
|
-- filter out duplicate headers
|
|
-- priority: conf headers > resolved userinfo vars > client headers
|
|
hdrMaps =
|
|
[ Map.fromList confHdrs,
|
|
Map.fromList userInfoToHdrs,
|
|
Map.fromList clientHdrs
|
|
]
|
|
headers = Map.toList $ foldr Map.union Map.empty hdrMaps
|
|
finalHeaders = addDefaultHeaders headers
|
|
initReq <- onLeft (HTTP.mkRequestEither $ tshow url) (throwRemoteSchemaHttp url)
|
|
let req =
|
|
initReq & set HTTP.method "POST"
|
|
& set HTTP.headers finalHeaders
|
|
& set HTTP.body (Just $ J.encode gqlReqUnparsed)
|
|
& set HTTP.timeout (HTTP.responseTimeoutMicro (timeout * 1000000))
|
|
|
|
Tracing.tracedHttpRequest req \req' -> do
|
|
(time, res) <- withElapsedTime $ liftIO $ try $ HTTP.performRequest req' manager
|
|
resp <- onLeft res (throwRemoteSchemaHttp url)
|
|
pure (time, mkSetCookieHeaders resp, resp ^. Wreq.responseBody)
|
|
where
|
|
ValidatedRemoteSchemaDef url hdrConf fwdClientHdrs timeout _mPrefix = rsdef
|
|
|
|
userInfoToHdrs = sessionVariablesToHeaders $ _uiSession userInfo
|
|
|
|
identityCustomizer :: RemoteSchemaCustomizer
|
|
identityCustomizer = RemoteSchemaCustomizer Nothing mempty mempty
|
|
|
|
getCustomizer :: IntrospectionResult -> Maybe RemoteSchemaCustomization -> RemoteSchemaCustomizer
|
|
getCustomizer _ Nothing = identityCustomizer
|
|
getCustomizer IntrospectionResult {..} (Just RemoteSchemaCustomization {..}) = RemoteSchemaCustomizer {..}
|
|
where
|
|
rootTypeNames =
|
|
if isNothing _rscRootFieldsNamespace
|
|
then catMaybes [Just irQueryRoot, irMutationRoot, irSubscriptionRoot]
|
|
else []
|
|
-- root type names should not be prefixed or suffixed unless
|
|
-- there is a custom root namespace field
|
|
scalarTypeNames = [intScalar, floatScalar, stringScalar, boolScalar, idScalar]
|
|
protectedTypeNames = scalarTypeNames ++ rootTypeNames
|
|
nameFilter name = not $ "__" `T.isPrefixOf` G.unName name || name `elem` protectedTypeNames
|
|
|
|
mkPrefixSuffixMap :: Maybe G.Name -> Maybe G.Name -> [G.Name] -> HashMap G.Name G.Name
|
|
mkPrefixSuffixMap mPrefix mSuffix names = Map.fromList $ case (mPrefix, mSuffix) of
|
|
(Nothing, Nothing) -> []
|
|
(Just prefix, Nothing) -> map (\name -> (name, prefix <> name)) names
|
|
(Nothing, Just suffix) -> map (\name -> (name, name <> suffix)) names
|
|
(Just prefix, Just suffix) -> map (\name -> (name, prefix <> name <> suffix)) names
|
|
|
|
RemoteSchemaIntrospection typeDefinitions = irDoc
|
|
typesToRename = filter nameFilter $ Map.keys typeDefinitions
|
|
typeRenameMap =
|
|
case _rscTypeNames of
|
|
Nothing -> Map.empty
|
|
Just RemoteTypeCustomization {..} ->
|
|
_rtcMapping <> mkPrefixSuffixMap _rtcPrefix _rtcSuffix typesToRename
|
|
|
|
typeFieldMap :: HashMap G.Name [G.Name] -- typeName -> fieldNames
|
|
typeFieldMap =
|
|
Map.mapMaybe getFieldsNames typeDefinitions
|
|
where
|
|
getFieldsNames = \case
|
|
G.TypeDefinitionObject G.ObjectTypeDefinition {..} -> Just $ G._fldName <$> _otdFieldsDefinition
|
|
G.TypeDefinitionInterface G.InterfaceTypeDefinition {..} -> Just $ G._fldName <$> _itdFieldsDefinition
|
|
_ -> Nothing
|
|
|
|
mkFieldRenameMap RemoteFieldCustomization {..} fieldNames =
|
|
_rfcMapping <> mkPrefixSuffixMap _rfcPrefix _rfcSuffix fieldNames
|
|
|
|
fieldRenameMap =
|
|
case _rscFieldNames of
|
|
Nothing -> Map.empty
|
|
Just fieldNameCustomizations ->
|
|
let customizationMap = Map.fromList $ map (\rfc -> (_rfcParentType rfc, rfc)) fieldNameCustomizations
|
|
in Map.intersectionWith mkFieldRenameMap customizationMap typeFieldMap
|
|
|
|
_rscNamespaceFieldName = _rscRootFieldsNamespace
|
|
_rscCustomizeTypeName = typeRenameMap
|
|
_rscCustomizeFieldName = fieldRenameMap
|
|
|
|
throwRemoteSchema ::
|
|
QErrM m =>
|
|
Text ->
|
|
m a
|
|
throwRemoteSchema = throw400 RemoteSchemaError
|
|
|
|
throwRemoteSchemaWithInternal ::
|
|
(QErrM m, J.ToJSON a) =>
|
|
Text ->
|
|
a ->
|
|
m b
|
|
throwRemoteSchemaWithInternal msg v =
|
|
let err = err400 RemoteSchemaError msg
|
|
in throwError err {qeInternal = Just $ ExtraInternal $ J.toJSON v}
|
|
|
|
throwRemoteSchemaHttp ::
|
|
QErrM m =>
|
|
URI ->
|
|
HTTP.HttpException ->
|
|
m a
|
|
throwRemoteSchemaHttp url =
|
|
throwRemoteSchemaWithInternal (T.pack httpExceptMsg) . httpExceptToJSON
|
|
where
|
|
httpExceptMsg =
|
|
"HTTP exception occurred while sending the request to " <> show url
|
|
|
|
-- Utility function for generating RemoteScemaIntrospection
|
|
|
|
newtype RemoteSchemaIntrospectionErr = RemoteSchemaIntrospectionErr
|
|
{ remoteSchemaIntrospectionErr :: String
|
|
}
|
|
deriving (Show, Typeable)
|
|
|
|
instance Exception RemoteSchemaIntrospectionErr where
|
|
displayException (RemoteSchemaIntrospectionErr msg) = "RemoteSchemaIntrospectionErr: " <> show msg
|
|
|
|
errorE :: String -> c
|
|
errorE = impureThrow . RemoteSchemaIntrospectionErr
|
|
|
|
getSchemaIntrospection :: HashMap RoleName (RoleContext GQLContext) -> Maybe RemoteSchemaIntrospection
|
|
getSchemaIntrospection gqlContext = do
|
|
RoleContext {..} <- Map.lookup adminRoleName gqlContext
|
|
fieldMap <- either (const Nothing) Just $ gqlQueryParser _rctxDefault $ fmap (fmap nameToVariable) $ G._todSelectionSet $ _grQuery introspectionQuery
|
|
RFRaw v <- OMap.lookup (mkUnNamespacedRootFieldAlias $$(G.litName "__schema")) fieldMap
|
|
fmap irDoc $ parseIntrospectionResult $ J.object [("data", J.object [("__schema", JO.fromOrdered v)])]
|
|
where
|
|
-- TODO: Look for a way to convert Name to Variable (using some default types) or make the
|
|
-- ParserFn general (i.e. type ParserFn a b = G.SelectionSet G.NoFragments b -> ...)
|
|
-- This value isn't used but we give it a type to be more clear about what is being ignored
|
|
nameToVariable :: G.Name -> Variable
|
|
nameToVariable n = errorE . T.unpack $ G.unName n <> " cannot be converted to Variable"
|