mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-17 20:41:49 +03:00
562b6ac43a
* add support for joining to remote interface and union fields * add test for making a remote relationship with an Union type * add CHANGELOG * remove the pretty-simple library * remove unused imports from Remote.hs * Apply suggestions from code review Co-authored-by: Brandon Simmons <brandon.m.simmons@gmail.com> * support remote relationships against enum fields as well * update CHANGELOG Co-authored-by: Brandon Simmons <brandon.m.simmons@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
438 lines
18 KiB
Haskell
438 lines
18 KiB
Haskell
{-# LANGUAGE ViewPatterns #-}
|
|
|
|
-- | Validate input queries against remote schemas.
|
|
|
|
module Hasura.RQL.DDL.RemoteRelationship.Validate
|
|
( validateRemoteRelationship
|
|
, errorToText
|
|
) where
|
|
|
|
import Hasura.Prelude hiding (first)
|
|
|
|
import qualified Data.HashMap.Strict as HM
|
|
import qualified Data.HashSet as HS
|
|
import qualified Language.GraphQL.Draft.Syntax as G
|
|
|
|
import Data.Foldable
|
|
import Data.Text.Extended
|
|
|
|
import Hasura.Backends.Postgres.SQL.Types
|
|
import Hasura.GraphQL.Parser.Column
|
|
import Hasura.GraphQL.Schema.Remote
|
|
import Hasura.GraphQL.Utils (getBaseTyWithNestedLevelsCount)
|
|
import Hasura.RQL.Types
|
|
import Hasura.SQL.Types
|
|
|
|
|
|
-- | An error validating the remote relationship.
|
|
data ValidationError
|
|
= RemoteSchemaNotFound !RemoteSchemaName
|
|
| CouldntFindRemoteField !G.Name !G.Name
|
|
| FieldNotFoundInRemoteSchema !G.Name
|
|
| NoSuchArgumentForRemote !G.Name
|
|
| MissingRequiredArgument !G.Name
|
|
| TypeNotFound !G.Name
|
|
| TableNotFound !QualifiedTable
|
|
| TableFieldNonexistent !QualifiedTable !FieldName
|
|
| ExpectedTypeButGot !G.GType !G.GType
|
|
| InvalidType !G.GType !Text
|
|
| InvalidVariable !G.Name !(HM.HashMap G.Name (ColumnInfo 'Postgres))
|
|
| NullNotAllowedHere
|
|
| InvalidGTypeForStripping !G.GType
|
|
| UnsupportedMultipleElementLists
|
|
| UnsupportedEnum
|
|
| InvalidGraphQLName !Text
|
|
|
|
errorToText :: ValidationError -> Text
|
|
errorToText = \case
|
|
RemoteSchemaNotFound name ->
|
|
"remote schema with name " <> name <<> " not found"
|
|
CouldntFindRemoteField name ty ->
|
|
"remote field with name " <> name <<> " and type " <> ty <<> " not found"
|
|
FieldNotFoundInRemoteSchema name ->
|
|
"field with name " <> name <<> " not found in remote schema"
|
|
NoSuchArgumentForRemote name ->
|
|
"argument with name " <> name <<> " not found in remote schema"
|
|
MissingRequiredArgument name ->
|
|
"required argument with name " <> name <<> " is missing"
|
|
TypeNotFound ty ->
|
|
"type with name " <> ty <<> " not found"
|
|
TableNotFound name ->
|
|
"table with name " <> name <<> " not found"
|
|
TableFieldNonexistent table fieldName ->
|
|
"field with name " <> fieldName <<> " not found in table " <>> table
|
|
ExpectedTypeButGot expTy actualTy ->
|
|
"expected type " <> G.getBaseType expTy <<> " but got " <>> G.getBaseType actualTy
|
|
InvalidType ty err ->
|
|
"type " <> G.getBaseType ty <<> err
|
|
InvalidVariable var _ ->
|
|
"variable " <> var <<> " is not found"
|
|
NullNotAllowedHere ->
|
|
"null is not allowed here"
|
|
InvalidGTypeForStripping ty ->
|
|
"type " <> G.getBaseType ty <<> " is invalid for stripping"
|
|
UnsupportedMultipleElementLists ->
|
|
"multiple elements in list value is not supported"
|
|
UnsupportedEnum ->
|
|
"enum value is not supported"
|
|
InvalidGraphQLName t ->
|
|
t <<> " is not a valid GraphQL identifier"
|
|
|
|
-- | Validate a remote relationship given a context.
|
|
validateRemoteRelationship
|
|
:: (MonadError ValidationError m)
|
|
=> RemoteRelationship
|
|
-> RemoteSchemaMap
|
|
-> [ColumnInfo 'Postgres]
|
|
-> m (RemoteFieldInfo 'Postgres)
|
|
validateRemoteRelationship remoteRelationship remoteSchemaMap pgColumns = do
|
|
let remoteSchemaName = rtrRemoteSchema remoteRelationship
|
|
table = rtrTable remoteRelationship
|
|
hasuraFields <- forM (toList $ rtrHasuraFields remoteRelationship) $
|
|
\fieldName -> onNothing (find ((==) fieldName . fromPGCol . pgiColumn) pgColumns) $
|
|
throwError $ TableFieldNonexistent table fieldName
|
|
pgColumnsVariables <- mapM (\(k,v) -> do
|
|
variableName <- pgColumnToVariable k
|
|
pure $ (variableName,v)
|
|
) $ HM.toList (mapFromL pgiColumn pgColumns)
|
|
let pgColumnsVariablesMap = HM.fromList pgColumnsVariables
|
|
(RemoteSchemaCtx rsName introspectionResult rsi _ _) <-
|
|
onNothing (HM.lookup remoteSchemaName remoteSchemaMap) $
|
|
throwError $ RemoteSchemaNotFound remoteSchemaName
|
|
let schemaDoc@(G.SchemaIntrospection originalDefns) = irDoc introspectionResult
|
|
queryRootName = irQueryRoot introspectionResult
|
|
queryRoot <- onNothing (lookupObject schemaDoc queryRootName) $
|
|
throwError $ FieldNotFoundInRemoteSchema queryRootName
|
|
(_, (leafParamMap, leafTypeMap)) <-
|
|
foldlM
|
|
(buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc)
|
|
(queryRoot,(mempty,mempty))
|
|
(unRemoteFields $ rtrRemoteField remoteRelationship)
|
|
pure $ RemoteFieldInfo
|
|
{ _rfiName = rtrName remoteRelationship
|
|
, _rfiParamMap = leafParamMap
|
|
, _rfiHasuraFields = HS.fromList hasuraFields
|
|
, _rfiRemoteFields = rtrRemoteField remoteRelationship
|
|
, _rfiRemoteSchema = rsi
|
|
-- adding the new types after stripping the values to the
|
|
-- schema document
|
|
, _rfiSchemaIntrospect = G.SchemaIntrospection $ originalDefns <> HM.elems leafTypeMap
|
|
, _rfiRemoteSchemaName = rsName
|
|
}
|
|
where
|
|
getObjTyInfoFromField schemaDoc field =
|
|
let baseTy = G.getBaseType (G._fldType field)
|
|
in lookupObject schemaDoc baseTy
|
|
isValidType schemaDoc field =
|
|
let baseTy = G.getBaseType (G._fldType field)
|
|
in
|
|
case (lookupType schemaDoc baseTy) of
|
|
Just (G.TypeDefinitionScalar _) -> True
|
|
Just (G.TypeDefinitionInterface _) -> True
|
|
Just (G.TypeDefinitionUnion _) -> True
|
|
Just (G.TypeDefinitionEnum _) -> True
|
|
_ -> False
|
|
buildRelationshipTypeInfo pgColumnsVariablesMap schemaDoc (objTyInfo,(_,typeMap)) fieldCall = do
|
|
objFldDefinition <- lookupField (fcName fieldCall) objTyInfo
|
|
let providedArguments = getRemoteArguments $ fcArguments fieldCall
|
|
validateRemoteArguments
|
|
(mapFromL G._ivdName (G._fldArgumentsDefinition objFldDefinition))
|
|
providedArguments
|
|
pgColumnsVariablesMap
|
|
schemaDoc
|
|
let eitherParamAndTypeMap =
|
|
runStateT
|
|
(stripInMap
|
|
remoteRelationship
|
|
schemaDoc
|
|
(mapFromL G._ivdName (G._fldArgumentsDefinition objFldDefinition))
|
|
providedArguments)
|
|
$ typeMap
|
|
(newParamMap, newTypeMap) <- onLeft eitherParamAndTypeMap $ throwError
|
|
innerObjTyInfo <- onNothing (getObjTyInfoFromField schemaDoc objFldDefinition) $
|
|
bool (throwError $
|
|
(InvalidType (G._fldType objFldDefinition) "only output type is expected"))
|
|
(pure objTyInfo)
|
|
(isValidType schemaDoc objFldDefinition)
|
|
pure
|
|
( innerObjTyInfo
|
|
, (newParamMap,newTypeMap))
|
|
|
|
-- | Return a map with keys deleted whose template argument is
|
|
-- specified as an atomic (variable, constant), keys which are kept
|
|
-- have their values modified by 'stripObject' or 'stripList'.
|
|
-- This function creates the 'HashMap G.Name G.InputValueDefinition' which modifies
|
|
-- the original input parameters (if any) of the remote node/table being used. Only
|
|
-- list or object types are preserved and other types are stripped off. The object or
|
|
-- list types are preserved because they can be merged, if any arguments are
|
|
-- provided by the user while querying a remote join field.
|
|
stripInMap
|
|
:: RemoteRelationship
|
|
-> G.SchemaIntrospection
|
|
-> HM.HashMap G.Name G.InputValueDefinition
|
|
-> HM.HashMap G.Name (G.Value G.Name)
|
|
-> StateT
|
|
(HashMap G.Name (G.TypeDefinition [G.Name]))
|
|
(Either ValidationError)
|
|
(HM.HashMap G.Name G.InputValueDefinition)
|
|
stripInMap remoteRelationship types schemaArguments providedArguments =
|
|
fmap
|
|
(HM.mapMaybe id)
|
|
(HM.traverseWithKey
|
|
(\name inpValInfo ->
|
|
case HM.lookup name providedArguments of
|
|
Nothing -> pure (Just inpValInfo)
|
|
Just value -> do
|
|
maybeNewGType <- stripValue remoteRelationship types (G._ivdType inpValInfo) value
|
|
pure
|
|
(fmap
|
|
(\newGType -> inpValInfo {G._ivdType = newGType})
|
|
maybeNewGType))
|
|
schemaArguments)
|
|
|
|
-- | Strip a value type completely, or modify it, if the given value
|
|
-- is atomic-ish.
|
|
stripValue
|
|
:: RemoteRelationship
|
|
-> G.SchemaIntrospection
|
|
-> G.GType
|
|
-> G.Value G.Name
|
|
-> StateT (HashMap G.Name (G.TypeDefinition [G.Name])) (Either ValidationError) (Maybe G.GType)
|
|
stripValue remoteRelationshipName types gtype value = do
|
|
case value of
|
|
G.VVariable {} -> pure Nothing
|
|
G.VInt {} -> pure Nothing
|
|
G.VFloat {} -> pure Nothing
|
|
G.VString {} -> pure Nothing
|
|
G.VBoolean {} -> pure Nothing
|
|
G.VNull {} -> pure Nothing
|
|
G.VEnum {} -> pure Nothing
|
|
G.VList values ->
|
|
case values of
|
|
[] -> pure Nothing
|
|
[gvalue] -> stripList remoteRelationshipName types gtype gvalue
|
|
_ -> lift (Left UnsupportedMultipleElementLists)
|
|
G.VObject keyPairs ->
|
|
fmap Just (stripObject remoteRelationshipName types gtype keyPairs)
|
|
|
|
-- -- | Produce a new type for the list, or strip it entirely.
|
|
stripList
|
|
:: RemoteRelationship
|
|
-> G.SchemaIntrospection
|
|
-> G.GType
|
|
-> G.Value G.Name
|
|
-> StateT (HashMap G.Name (G.TypeDefinition [G.Name])) (Either ValidationError) (Maybe G.GType)
|
|
stripList remoteRelationshipName types originalOuterGType value =
|
|
case originalOuterGType of
|
|
G.TypeList nullability innerGType -> do
|
|
maybeNewInnerGType <- stripValue remoteRelationshipName types innerGType value
|
|
pure (G.TypeList nullability <$> maybeNewInnerGType)
|
|
_ -> lift (Left (InvalidGTypeForStripping originalOuterGType))
|
|
|
|
-- -- | Produce a new type for the given InpValInfo, modified by
|
|
-- -- 'stripInMap'. Objects can't be deleted entirely, just keys of an
|
|
-- -- object.
|
|
stripObject
|
|
:: RemoteRelationship
|
|
-> G.SchemaIntrospection
|
|
-> G.GType
|
|
-> HashMap G.Name (G.Value G.Name)
|
|
-> StateT (HashMap G.Name (G.TypeDefinition [G.Name])) (Either ValidationError) G.GType
|
|
stripObject remoteRelationshipName schemaDoc originalGtype templateArguments =
|
|
case originalGtype of
|
|
G.TypeNamed nullability originalNamedType ->
|
|
case lookupType schemaDoc (G.getBaseType originalGtype) of
|
|
Just (G.TypeDefinitionInputObject originalInpObjTyInfo) -> do
|
|
let originalSchemaArguments =
|
|
mapFromL G._ivdName $ G._iotdValueDefinitions originalInpObjTyInfo
|
|
newNamedType =
|
|
renameNamedType
|
|
(renameTypeForRelationship remoteRelationshipName)
|
|
originalNamedType
|
|
newSchemaArguments <-
|
|
stripInMap
|
|
remoteRelationshipName
|
|
schemaDoc
|
|
originalSchemaArguments
|
|
templateArguments
|
|
let newInpObjTyInfo =
|
|
originalInpObjTyInfo
|
|
{ G._iotdValueDefinitions = HM.elems newSchemaArguments
|
|
, G._iotdName = newNamedType
|
|
}
|
|
newGtype = G.TypeNamed nullability newNamedType
|
|
modify (HM.insert newNamedType (G.TypeDefinitionInputObject newInpObjTyInfo))
|
|
pure newGtype
|
|
_ -> lift (Left (InvalidGTypeForStripping originalGtype))
|
|
_ -> lift (Left (InvalidGTypeForStripping originalGtype))
|
|
|
|
-- -- | Produce a new name for a type, used when stripping the schema
|
|
-- -- types for a remote relationship.
|
|
-- TODO: Consider a separator character to avoid conflicts. (from master)
|
|
renameTypeForRelationship :: RemoteRelationship -> Text -> Text
|
|
renameTypeForRelationship rtr text =
|
|
text <> "_remote_rel_" <> name
|
|
where name = schema <> "_" <> table <> remoteRelationshipNameToText (rtrName rtr)
|
|
QualifiedObject (SchemaName schema) (TableName table) = rtrTable rtr
|
|
|
|
-- | Rename a type.
|
|
renameNamedType :: (Text -> Text) -> G.Name -> G.Name
|
|
renameNamedType rename =
|
|
G.unsafeMkName . rename . G.unName
|
|
|
|
-- | Convert a field name to a variable name.
|
|
pgColumnToVariable :: (MonadError ValidationError m) => PGCol -> m G.Name
|
|
pgColumnToVariable pgCol =
|
|
let pgColText = getPGColTxt pgCol
|
|
in maybe (throwError $ InvalidGraphQLName pgColText) pure $ G.mkName pgColText
|
|
|
|
-- | Lookup the field in the schema.
|
|
lookupField
|
|
:: (MonadError ValidationError m)
|
|
=> G.Name
|
|
-> G.ObjectTypeDefinition
|
|
-> m G.FieldDefinition
|
|
lookupField name objFldInfo = viaObject objFldInfo
|
|
where
|
|
viaObject =
|
|
maybe (throwError (CouldntFindRemoteField name $ G._otdName objFldInfo)) pure .
|
|
lookup name .
|
|
HM.toList .
|
|
mapFromL G._fldName .
|
|
G._otdFieldsDefinition
|
|
|
|
-- | Validate remote input arguments against the remote schema.
|
|
validateRemoteArguments
|
|
:: (MonadError ValidationError m)
|
|
=> HM.HashMap G.Name G.InputValueDefinition
|
|
-> HM.HashMap G.Name (G.Value G.Name)
|
|
-> HM.HashMap G.Name (ColumnInfo 'Postgres)
|
|
-> G.SchemaIntrospection
|
|
-> m ()
|
|
validateRemoteArguments expectedArguments providedArguments permittedVariables schemaDocument = do
|
|
traverse_ validateProvided (HM.toList providedArguments)
|
|
-- Not neccessary to validate if all required args are provided in the relationship
|
|
-- traverse validateExpected (HM.toList expectedArguments)
|
|
where
|
|
validateProvided (providedName, providedValue) =
|
|
case HM.lookup providedName expectedArguments of
|
|
Nothing -> throwError (NoSuchArgumentForRemote providedName)
|
|
Just (G._ivdType -> expectedType) ->
|
|
validateType permittedVariables providedValue expectedType schemaDocument
|
|
|
|
unwrapGraphQLType :: G.GType -> G.GType
|
|
unwrapGraphQLType = \case
|
|
G.TypeList _ lt -> lt
|
|
nt -> nt
|
|
|
|
-- | Validate a value against a type.
|
|
validateType
|
|
:: (MonadError ValidationError m)
|
|
=> HM.HashMap G.Name (ColumnInfo 'Postgres)
|
|
-> G.Value G.Name
|
|
-> G.GType
|
|
-> G.SchemaIntrospection
|
|
-> m ()
|
|
validateType permittedVariables value expectedGType schemaDocument =
|
|
case value of
|
|
G.VVariable variable ->
|
|
case HM.lookup variable permittedVariables of
|
|
Nothing -> throwError (InvalidVariable variable permittedVariables)
|
|
Just fieldInfo -> do
|
|
namedType <- columnInfoToNamedType fieldInfo
|
|
isTypeCoercible (mkGraphQLType namedType) expectedGType
|
|
G.VInt {} -> do
|
|
intScalarGType <- mkGraphQLType <$> mkScalarTy PGInteger
|
|
isTypeCoercible intScalarGType expectedGType
|
|
G.VFloat {} -> do
|
|
floatScalarGType <- mkGraphQLType <$> mkScalarTy PGFloat
|
|
isTypeCoercible floatScalarGType expectedGType
|
|
G.VBoolean {} -> do
|
|
boolScalarGType <- mkGraphQLType <$> mkScalarTy PGBoolean
|
|
isTypeCoercible boolScalarGType expectedGType
|
|
G.VNull -> throwError NullNotAllowedHere
|
|
G.VString {} -> do
|
|
stringScalarGType <- mkGraphQLType <$> mkScalarTy PGText
|
|
isTypeCoercible stringScalarGType expectedGType
|
|
G.VEnum _ -> throwError UnsupportedEnum
|
|
G.VList values -> do
|
|
case values of
|
|
[] -> pure ()
|
|
[_] -> pure ()
|
|
_ -> throwError UnsupportedMultipleElementLists
|
|
assertListType expectedGType
|
|
for_
|
|
values
|
|
(\val ->
|
|
validateType permittedVariables val (unwrapGraphQLType expectedGType) schemaDocument)
|
|
G.VObject values ->
|
|
for_
|
|
(HM.toList values)
|
|
(\(name,val) ->
|
|
let expectedNamedType = G.getBaseType expectedGType
|
|
in
|
|
case lookupType schemaDocument expectedNamedType of
|
|
Nothing -> throwError $ TypeNotFound expectedNamedType
|
|
Just typeInfo ->
|
|
case typeInfo of
|
|
G.TypeDefinitionInputObject inpObjTypeInfo ->
|
|
let objectTypeDefnsMap =
|
|
mapFromL G._ivdName $ G._iotdValueDefinitions inpObjTypeInfo
|
|
in
|
|
case HM.lookup name objectTypeDefnsMap of
|
|
Nothing -> throwError $ NoSuchArgumentForRemote name
|
|
Just (G._ivdType -> expectedType) ->
|
|
validateType permittedVariables val expectedType schemaDocument
|
|
_ -> do
|
|
throwError $ InvalidType (mkGraphQLType name) "not an input object type")
|
|
where
|
|
mkGraphQLType =
|
|
G.TypeNamed (G.Nullability False)
|
|
|
|
mkScalarTy scalarType = do
|
|
eitherScalar <- runExceptT $ mkScalarTypeName scalarType
|
|
case eitherScalar of
|
|
Left _ -> throwError $ InvalidGraphQLName $ toSQLTxt scalarType
|
|
Right s -> pure s
|
|
|
|
isTypeCoercible
|
|
:: (MonadError ValidationError m)
|
|
=> G.GType
|
|
-> G.GType
|
|
-> m ()
|
|
isTypeCoercible actualType expectedType =
|
|
-- The GraphQL spec says that, a singleton type can be coerced into an array
|
|
-- type. Which means that if the 'actualType' is a singleton type, like
|
|
-- 'Int' we should be able to join this with a remote node, which expects an
|
|
-- input argument of type '[Int]'
|
|
-- http://spec.graphql.org/June2018/#sec-Type-System.List
|
|
let (actualBaseType, actualNestingLevel) = getBaseTyWithNestedLevelsCount actualType
|
|
(expectedBaseType, expectedNestingLevel) = getBaseTyWithNestedLevelsCount expectedType
|
|
in
|
|
if | actualBaseType /= expectedBaseType -> raiseValidationError
|
|
-- we cannot coerce two types with different nesting levels,
|
|
-- for example, we cannot coerce [Int] to [[Int]]
|
|
| (actualNestingLevel == expectedNestingLevel || actualNestingLevel == 0) -> pure ()
|
|
| otherwise -> raiseValidationError
|
|
where
|
|
raiseValidationError = throwError $ ExpectedTypeButGot expectedType actualType
|
|
|
|
assertListType :: (MonadError ValidationError m) => G.GType -> m ()
|
|
assertListType actualType =
|
|
unless (G.isListType actualType)
|
|
(throwError $ InvalidType actualType "is not a list type")
|
|
|
|
-- | Convert a field info to a named type, if possible.
|
|
columnInfoToNamedType
|
|
:: (MonadError ValidationError m)
|
|
=> ColumnInfo 'Postgres
|
|
-> m G.Name
|
|
columnInfoToNamedType pci =
|
|
case pgiType pci of
|
|
PGColumnScalar scalarType -> do
|
|
eitherScalar <- runExceptT $ mkScalarTypeName scalarType
|
|
case eitherScalar of
|
|
Left _ -> throwError $ InvalidGraphQLName $ toSQLTxt scalarType
|
|
Right s -> pure s
|
|
_ -> throwError UnsupportedEnum
|