mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
server: add validation for query collections
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3658 GitOrigin-RevId: 3c644da15c92cac16356985d0fe0c6adb7001862
This commit is contained in:
parent
a2da867fc8
commit
0775c00b0d
@ -4,6 +4,7 @@
|
||||
|
||||
### Bug fixes and improvements
|
||||
|
||||
- server: Queries present in query collections, such as allow-list, and rest-endpoints are now validated (against the schema)
|
||||
- server: Redesigns internal implementation of webhook transforms.
|
||||
- server: improve SQL generation for BigQuery backend queries involving `Orderby`.
|
||||
- server: fix regression where remote relationships would get exposed over Relay, which is unsupported
|
||||
|
@ -53,6 +53,12 @@ create_query_collection
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
The queries in query collections are validated against the schema. So, adding an invalid
|
||||
query would result in inconsistent metadata error. As the query collection is used in allowlists and
|
||||
REST endpoints, they are validated as well.
|
||||
|
||||
.. _metadata_create_query_collection_syntax:
|
||||
|
||||
Args Syntax
|
||||
|
@ -52,6 +52,12 @@ create_query_collection
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
The queries in query collections are validated against the schema. So, adding an invalid
|
||||
query would result in inconsistent metadata error. As the query collection is used in allowlists and
|
||||
REST endpoints, they are validated as well.
|
||||
|
||||
.. _schema_metadata_create_query_collection_syntax:
|
||||
|
||||
Args Syntax
|
||||
|
@ -50,6 +50,9 @@ You can add or remove a operation in the allow-list in two ways:
|
||||
|
||||
.. note::
|
||||
|
||||
* The allow list queries are validated against the schema. So, adding an invalid query will result in inconsistent
|
||||
metadata error.
|
||||
|
||||
* ``__typename`` introspection fields will be ignored when adding operations and comparing them to the allow-list.
|
||||
|
||||
* Any introspection queries that your client apps require will have to be explicitly added to the allow-list
|
||||
|
@ -3,6 +3,7 @@ module Hasura.GraphQL.Analyse
|
||||
FieldAnalysis (..),
|
||||
FieldDef (..),
|
||||
analyzeGraphqlQuery,
|
||||
getAllAnalysisErrs,
|
||||
)
|
||||
where
|
||||
|
||||
@ -57,6 +58,24 @@ type FieldName = Name
|
||||
|
||||
type VarName = Name
|
||||
|
||||
analyzeGraphqlQuery :: ExecutableDefinition Name -> RemoteSchemaIntrospection -> Maybe (Analysis Name)
|
||||
analyzeGraphqlQuery (G.ExecutableDefinitionOperation (G.OperationDefinitionTyped td)) sc = do
|
||||
let t = (G._todType td,) <$> G._todSelectionSet td
|
||||
varDefs = G._todVariableDefinitions td
|
||||
varMapList = map (\v -> (G._vdName v, (G._vdType v, G._vdDefaultValue v))) varDefs
|
||||
varMap = Map.fromList varMapList
|
||||
(fieldMap, errs) = getFieldsMap sc t
|
||||
pure Analysis {_aFields = fieldMap, _aVars = varMap, _aErrs = errs}
|
||||
analyzeGraphqlQuery _ _ = Nothing
|
||||
|
||||
getAllAnalysisErrs :: Analysis Name -> [Text]
|
||||
getAllAnalysisErrs Analysis {..} = _aErrs <> getFieldErrs (OMap.toList _aFields) []
|
||||
where
|
||||
getFieldErrs :: [(G.Name, (FieldDef, Maybe (FieldAnalysis G.Name)))] -> [Text] -> [Text]
|
||||
getFieldErrs [] lst = lst
|
||||
getFieldErrs ((_, (_, Just FieldAnalysis {..})) : xs) lst = _fErrs <> (getFieldErrs (OMap.toList _fFields) []) <> (getFieldErrs xs lst)
|
||||
getFieldErrs ((_, (_, Nothing)) : xs) lst = getFieldErrs xs lst
|
||||
|
||||
-- | inserts in field map, if there is already a key, then take a union of the fields inside
|
||||
-- i.e. for the following graphql query:
|
||||
-- query MyQuery {
|
||||
@ -78,15 +97,6 @@ safeInsertInFieldMap ::
|
||||
safeInsertInFieldMap m (k, v) =
|
||||
OMap.insertWith (\(fdef, (f1)) (_, (f2)) -> (fdef, f1 <> f2)) k v m
|
||||
|
||||
analyzeGraphqlQuery :: ExecutableDefinition Name -> RemoteSchemaIntrospection -> Maybe (Analysis Name)
|
||||
analyzeGraphqlQuery (G.ExecutableDefinitionOperation (G.OperationDefinitionTyped td)) sc = do
|
||||
let t = (G._todType td,) <$> G._todSelectionSet td
|
||||
varDefs = G._todVariableDefinitions td
|
||||
varMap = foldr (\G.VariableDefinition {..} m -> Map.insert _vdName (_vdType, _vdDefaultValue) m) mempty varDefs
|
||||
(fieldMap, errs) = getFieldsMap sc t
|
||||
pure Analysis {_aFields = fieldMap, _aVars = varMap, _aErrs = errs}
|
||||
analyzeGraphqlQuery _ _ = Nothing
|
||||
|
||||
getFieldName :: Field frag var -> Name
|
||||
getFieldName f = fromMaybe (G._fName f) (G._fAlias f)
|
||||
|
||||
@ -105,6 +115,12 @@ getFieldsMap rs ss =
|
||||
where
|
||||
fields = mapMaybe (\(o, s) -> (o,) <$> field s) ss
|
||||
|
||||
getFieldsTypeM :: Name -> TypeDefinition possibleTypes inputType -> Maybe GType
|
||||
getFieldsTypeM fieldName operationDefinitionSum = do
|
||||
operationDefinitionObject <- asObjectTypeDefinition operationDefinitionSum
|
||||
fieldDefinition <- find ((== fieldName) . G._fldName) $ G._otdFieldsDefinition operationDefinitionObject
|
||||
pure $ G._fldType fieldDefinition
|
||||
|
||||
lookupRoot ::
|
||||
RemoteSchemaIntrospection ->
|
||||
(G.OperationType, G.Field frag var) ->
|
||||
@ -114,14 +130,37 @@ lookupRoot rs (ot, f) = do
|
||||
fieldName = G._fName f
|
||||
fieldTypeM = do
|
||||
operationDefinitionSum <- lookupRS rs rootFieldName
|
||||
operationDefinitionObject <- asObjectTypeDefinition operationDefinitionSum
|
||||
fieldDefinition <- find ((== fieldName) . G._fldName) $ G._otdFieldsDefinition operationDefinitionObject
|
||||
pure $ G._fldType fieldDefinition
|
||||
getFieldsTypeM fieldName operationDefinitionSum
|
||||
|
||||
case fieldTypeM of
|
||||
Nothing -> Right $ ["Couldn't find field " <> G.unName fieldName <> " in root field " <> G.unName rootFieldName]
|
||||
Nothing ->
|
||||
case isMetaField fieldName True of
|
||||
Nothing -> Right $ ["Couldn't find field " <> G.unName fieldName <> " in root field " <> G.unName rootFieldName]
|
||||
Just fld -> lookupDefinition rs fld f
|
||||
Just fieldType -> lookupDefinition rs fieldType f
|
||||
|
||||
isMetaField :: Name -> Bool -> Maybe GType
|
||||
isMetaField nam isRoot = do
|
||||
n <- fieldTypeMetaFields $ nam
|
||||
case G.unName nam of
|
||||
"__schema" -> if isRoot then Just $ mkGType n else Nothing
|
||||
"__type" -> if isRoot then Just $ mkGType n else Nothing
|
||||
"__typename" -> Just $ mkGType n
|
||||
_ -> Nothing
|
||||
where
|
||||
mkGType :: Name -> GType
|
||||
mkGType fName =
|
||||
G.TypeNamed
|
||||
(G.Nullability {unNullability = False})
|
||||
fName
|
||||
|
||||
fieldTypeMetaFields :: Name -> Maybe Name
|
||||
fieldTypeMetaFields nam
|
||||
| (Just nam) == G.mkName "__schema" = G.mkName "__Schema"
|
||||
| (Just nam) == G.mkName "__type" = G.mkName "__Type"
|
||||
| (Just nam) == G.mkName "__typename" = G.mkName "String"
|
||||
| otherwise = Nothing
|
||||
|
||||
lookupDefinition ::
|
||||
RemoteSchemaIntrospection ->
|
||||
GType ->
|
||||
@ -173,8 +212,13 @@ getDefinition ::
|
||||
getDefinition rs td sels =
|
||||
(td,) $ case td of
|
||||
(G.TypeDefinitionObject otd) -> do
|
||||
ps <- traverse (\sel -> fmap (mkFieldAnalysis rs sel) (lookupFieldBySelection sel (G._otdFieldsDefinition otd))) sels
|
||||
pure $ fold $ catMaybes ps
|
||||
ps <-
|
||||
for
|
||||
sels
|
||||
\sel -> case (lookupFieldBySelection sel otd) of
|
||||
Left txts -> pure $ FieldAnalysis mempty mempty txts
|
||||
Right fd -> (mkFieldAnalysis rs sel fd)
|
||||
pure $ fold ps
|
||||
_ -> Nothing
|
||||
|
||||
itrListWith :: GType -> (Name -> p) -> p
|
||||
@ -219,6 +263,21 @@ mkFieldAnalysis rs (G.SelectionField f) fd = do
|
||||
_fErrs = fErrs
|
||||
}
|
||||
|
||||
lookupFieldBySelection :: G.Selection frag0 var0 -> [G.FieldDefinition RemoteSchemaInputValueDefinition] -> Maybe (G.FieldDefinition RemoteSchemaInputValueDefinition)
|
||||
lookupFieldBySelection (G.SelectionField f) = find \d -> G._fName f == G._fldName d
|
||||
lookupFieldBySelection _ = const Nothing
|
||||
lookupFieldBySelection :: G.Selection frag0 var0 -> G.ObjectTypeDefinition RemoteSchemaInputValueDefinition -> Either [Text] (G.FieldDefinition RemoteSchemaInputValueDefinition)
|
||||
lookupFieldBySelection (G.SelectionField f) otd = case find (\d -> G._fName f == G._fldName d) lst of
|
||||
Nothing -> case isMetaField (G._fName f) False of
|
||||
Nothing -> Left ["Couldn't find definition for field " <> G.unName (getFieldName f) <> " in " <> G.unName (G._otdName otd)]
|
||||
Just gt -> Right $ mkFieldDef (G._fName f) gt
|
||||
Just fd -> Right fd
|
||||
where
|
||||
lst = G._otdFieldsDefinition otd
|
||||
mkFieldDef :: G.Name -> GType -> G.FieldDefinition RemoteSchemaInputValueDefinition
|
||||
mkFieldDef gName gt =
|
||||
G.FieldDefinition
|
||||
{ _fldDescription = Nothing,
|
||||
_fldName = gName,
|
||||
_fldArgumentsDefinition = [],
|
||||
_fldType = gt,
|
||||
_fldDirectives = []
|
||||
}
|
||||
lookupFieldBySelection _ _ = Left []
|
||||
|
@ -33,7 +33,7 @@ import Hasura.Base.Error
|
||||
import Hasura.GraphQL.Context
|
||||
import Hasura.GraphQL.Namespace (mkUnNamespacedRootFieldAlias)
|
||||
import Hasura.GraphQL.Parser.Collect ()
|
||||
import Hasura.GraphQL.Parser.Schema (Variable)
|
||||
import Hasura.GraphQL.Parser.Schema (InputValue (..), Variable (..), VariableInfo (..))
|
||||
-- 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
|
||||
@ -569,9 +569,13 @@ getSchemaIntrospection gqlContext = do
|
||||
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)])]
|
||||
|
||||
nameToVariable :: G.Name -> Variable
|
||||
nameToVariable n = Variable vInf vTyp vVal
|
||||
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"
|
||||
vInf = VIRequired n
|
||||
vTyp = G.TypeNamed (G.Nullability True) n
|
||||
vVal = dummyValue
|
||||
|
||||
dummyValue :: InputValue Void
|
||||
dummyValue = JSONValue $ J.String $ "nameToVariable is being called"
|
||||
|
@ -23,6 +23,7 @@ module Hasura.Prelude
|
||||
hoistEither,
|
||||
readJson,
|
||||
tshow,
|
||||
hashNub,
|
||||
|
||||
-- * Trace debugging
|
||||
ltrace,
|
||||
@ -92,6 +93,7 @@ import Data.HashMap.Strict qualified as Map
|
||||
import Data.HashMap.Strict.InsOrd as M (InsOrdHashMap)
|
||||
import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||
import Data.HashSet as M (HashSet)
|
||||
import Data.HashSet qualified as HSet
|
||||
import Data.Hashable (hashWithSalt)
|
||||
import Data.Hashable as M (Hashable)
|
||||
import Data.List as M
|
||||
@ -293,3 +295,12 @@ ltrace lbl x = Debug.trace (lbl <> ": " <> TL.unpack (PS.pShow x)) x
|
||||
ltraceM :: Applicative m => Show a => String -> a -> m ()
|
||||
ltraceM lbl x = Debug.traceM (lbl <> ": " <> TL.unpack (PS.pShow x))
|
||||
{-# WARNING ltraceM "ltraceM left in code" #-}
|
||||
|
||||
-- | Remove duplicates from a list. Like 'nub' but runs in @O(n * log_16(n))@
|
||||
-- time and requires 'Hashable' and `Eq` instances. hashNub is faster than
|
||||
-- ordNub when there're not so many different values in the list.
|
||||
--
|
||||
-- >>> hashNub [1,3,2,9,4,1,5,7,3,3,1,2,5,4,3,2,1,0]
|
||||
-- [0,1,2,3,4,5,7,9]
|
||||
hashNub :: (Hashable a, Eq a) => [a] -> [a]
|
||||
hashNub = HSet.toList . HSet.fromList
|
||||
|
@ -438,6 +438,7 @@ purgeMetadataObj = \case
|
||||
MOEndpoint epName -> dropEndpointInMetadata epName
|
||||
MOInheritedRole role -> dropInheritedRoleInMetadata role
|
||||
MOHostTlsAllowlist host -> dropHostFromAllowList host
|
||||
MOQueryCollectionsQuery cName lq -> dropListedQueryFromQueryCollections cName lq
|
||||
where
|
||||
handleSourceObj :: forall b. BackendMetadata b => SourceName -> SourceMetadataObjId b -> MetadataModifier
|
||||
handleSourceObj source = \case
|
||||
@ -453,6 +454,42 @@ purgeMetadataObj = \case
|
||||
MTOComputedField ccn -> dropComputedFieldInMetadata ccn
|
||||
MTORemoteRelationship rn -> dropRemoteRelationshipInMetadata rn
|
||||
|
||||
dropListedQueryFromQueryCollections :: CollectionName -> ListedQuery -> MetadataModifier
|
||||
dropListedQueryFromQueryCollections cName lq = MetadataModifier $ removeAndCleanupMetadata
|
||||
where
|
||||
removeAndCleanupMetadata m =
|
||||
let newQueryCollection = filteredCollection (_metaQueryCollections m)
|
||||
-- QueryCollections = InsOrdHashMap CollectionName CreateCollection
|
||||
filteredCollection :: QueryCollections -> QueryCollections
|
||||
filteredCollection qc = OMap.filter (isNonEmptyCC) $ OMap.adjust (collectionModifier) (cName) qc
|
||||
|
||||
collectionModifier :: CreateCollection -> CreateCollection
|
||||
collectionModifier cc@CreateCollection {..} =
|
||||
cc
|
||||
{ _ccDefinition =
|
||||
let oldQueries = _cdQueries _ccDefinition
|
||||
in _ccDefinition
|
||||
{ _cdQueries = filter (/= lq) oldQueries
|
||||
}
|
||||
}
|
||||
|
||||
isNonEmptyCC :: CreateCollection -> Bool
|
||||
isNonEmptyCC = not . null . _cdQueries . _ccDefinition
|
||||
|
||||
cleanupAllowList :: MetadataAllowlist -> MetadataAllowlist
|
||||
cleanupAllowList = OMap.filterWithKey (\_ _ -> OMap.member cName newQueryCollection)
|
||||
|
||||
cleanupRESTEndpoints :: Endpoints -> Endpoints
|
||||
cleanupRESTEndpoints endpoints = OMap.filter (not . isFaultyQuery . _edQuery . _ceDefinition) endpoints
|
||||
|
||||
isFaultyQuery :: QueryReference -> Bool
|
||||
isFaultyQuery QueryReference {..} = _qrCollectionName == cName && _qrQueryName == (_lqName lq)
|
||||
in m
|
||||
{ _metaQueryCollections = newQueryCollection,
|
||||
_metaAllowlist = cleanupAllowList (_metaAllowlist m),
|
||||
_metaRestEndpoints = cleanupRESTEndpoints (_metaRestEndpoints m)
|
||||
}
|
||||
|
||||
runGetCatalogState ::
|
||||
(MonadMetadataStorageQueryAPI m) => GetCatalogState -> m EncJSON
|
||||
runGetCatalogState _ =
|
||||
|
@ -281,6 +281,9 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
|
||||
endpointObject :: EndpointMetadata q -> MetadataObject
|
||||
endpointObject md = MetadataObject (endpointObjId md) (toJSON $ OMap.lookup (_ceName md) $ _metaRestEndpoints metadata)
|
||||
|
||||
listedQueryObjects :: (CollectionName, ListedQuery) -> MetadataObject
|
||||
listedQueryObjects (cName, lq) = MetadataObject (MOQueryCollectionsQuery cName lq) (toJSON lq)
|
||||
|
||||
-- Cases of urls that generate invalid segments:
|
||||
|
||||
hasInvalidSegments :: EndpointMetadata query -> Bool
|
||||
@ -299,8 +302,11 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
|
||||
ambiguousF' ep = MetadataObject (endpointObjId ep) (toJSON ep)
|
||||
ambiguousF mds = AmbiguousRestEndpoints (commaSeparated $ map _ceUrl mds) (map ambiguousF' mds)
|
||||
ambiguousRestEndpoints = map (ambiguousF . S.elems . snd) $ ambiguousPathsGrouped endpoints
|
||||
|
||||
maybeRS = getSchemaIntrospection gqlContext
|
||||
inconsistentRestQueries = getInconsistentRestQueries maybeRS endpoints endpointObject
|
||||
queryCollections = _boQueryCollections resolvedOutputs
|
||||
allowLists = HS.toList . iaGlobal . _boAllowlist $ resolvedOutputs
|
||||
inconsistentQueryCollections = getInconsistentQueryCollections maybeRS queryCollections listedQueryObjects endpoints allowLists
|
||||
|
||||
returnA
|
||||
-<
|
||||
@ -328,12 +334,13 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
|
||||
<> duplicateRestVariables
|
||||
<> invalidRestSegments
|
||||
<> ambiguousRestEndpoints
|
||||
<> inconsistentRestQueries,
|
||||
<> inconsistentQueryCollections,
|
||||
scApiLimits = _boApiLimits resolvedOutputs,
|
||||
scMetricsConfig = _boMetricsConfig resolvedOutputs,
|
||||
scMetadataResourceVersion = Nothing,
|
||||
scSetGraphqlIntrospectionOptions = _metaSetGraphqlIntrospectionOptions metadata,
|
||||
scTlsAllowlist = _boTlsAllowlist resolvedOutputs
|
||||
scTlsAllowlist = _boTlsAllowlist resolvedOutputs,
|
||||
scQueryCollections = _boQueryCollections resolvedOutputs
|
||||
}
|
||||
where
|
||||
getSourceConfigIfNeeded ::
|
||||
@ -833,7 +840,8 @@ buildSchemaCacheRule logger env = proc (metadata, invalidationKeys) -> do
|
||||
_boApiLimits = apiLimits,
|
||||
_boMetricsConfig = metricsConfig,
|
||||
_boRoles = mapFromL _rRoleName $ _unOrderedRoles orderedRoles,
|
||||
_boTlsAllowlist = (networkTlsAllowlist networkConfig)
|
||||
_boTlsAllowlist = (networkTlsAllowlist networkConfig),
|
||||
_boQueryCollections = collections
|
||||
}
|
||||
|
||||
mkEndpointMetadataObject (name, createEndpoint) =
|
||||
|
@ -25,6 +25,7 @@ module Hasura.RQL.DDL.Schema.Cache.Common
|
||||
boCronTriggers,
|
||||
boCustomTypes,
|
||||
boEndpoints,
|
||||
boQueryCollections,
|
||||
boRemoteSchemas,
|
||||
boRoles,
|
||||
boSources,
|
||||
@ -157,7 +158,8 @@ data BuildOutputs = BuildOutputs
|
||||
_boApiLimits :: !ApiLimit,
|
||||
_boMetricsConfig :: !MetricsConfig,
|
||||
_boRoles :: !(HashMap RoleName Role),
|
||||
_boTlsAllowlist :: ![TlsAllow]
|
||||
_boTlsAllowlist :: ![TlsAllow],
|
||||
_boQueryCollections :: !QueryCollections
|
||||
}
|
||||
|
||||
$(makeLenses ''BuildOutputs)
|
||||
|
@ -216,6 +216,12 @@ deleteMetadataObject = \case
|
||||
MOActionPermission name role -> boActions . ix name . aiPermissions %~ M.delete role
|
||||
MOInheritedRole name -> boRoles %~ M.delete name
|
||||
MOHostTlsAllowlist host -> removeHostFromAllowList host
|
||||
MOQueryCollectionsQuery cName lq -> \bo@BuildOutputs {..} ->
|
||||
bo
|
||||
{ _boEndpoints = removeEndpointsUsingQueryCollection lq _boEndpoints,
|
||||
_boAllowlist = removeFromAllowList lq _boAllowlist,
|
||||
_boQueryCollections = removeFromQueryCollections cName lq _boQueryCollections
|
||||
}
|
||||
where
|
||||
removeHostFromAllowList hst bo =
|
||||
bo
|
||||
@ -243,3 +249,34 @@ deleteMetadataObject = \case
|
||||
MTOTrigger name -> tiEventTriggerInfoMap %~ M.delete name
|
||||
MTOPerm roleName permType -> withPermType permType \accessor ->
|
||||
tiRolePermInfoMap . ix roleName . permAccToLens accessor .~ Nothing
|
||||
|
||||
removeFromQueryCollections :: CollectionName -> ListedQuery -> QueryCollections -> QueryCollections
|
||||
removeFromQueryCollections cName lq qc =
|
||||
let collectionModifier :: CreateCollection -> CreateCollection
|
||||
collectionModifier cc@CreateCollection {..} =
|
||||
cc
|
||||
{ _ccDefinition =
|
||||
let oldQueries = _cdQueries _ccDefinition
|
||||
in _ccDefinition
|
||||
{ _cdQueries = filter (/= lq) oldQueries
|
||||
}
|
||||
}
|
||||
in OMap.adjust collectionModifier cName qc
|
||||
|
||||
removeEndpointsUsingQueryCollection :: ListedQuery -> HashMap EndpointName (EndpointMetadata GQLQueryWithText) -> HashMap EndpointName (EndpointMetadata GQLQueryWithText)
|
||||
removeEndpointsUsingQueryCollection lq endpointMap =
|
||||
case maybeEndpoint of
|
||||
Just (n, _) -> M.delete n endpointMap
|
||||
Nothing -> endpointMap
|
||||
where
|
||||
q = _lqQuery lq
|
||||
maybeEndpoint = find (\(_, def) -> (_edQuery . _ceDefinition) def == q) (M.toList endpointMap)
|
||||
|
||||
removeFromAllowList :: ListedQuery -> InlinedAllowlist -> InlinedAllowlist
|
||||
removeFromAllowList lq aList =
|
||||
let oldAList = iaGlobal aList
|
||||
gqlQry = NormalizedQuery . unGQLQuery . getGQLQuery . _lqQuery $ lq
|
||||
newAList = HS.delete gqlQry oldAList
|
||||
in aList
|
||||
{ iaGlobal = newAList
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
module Hasura.RQL.Types.Allowlist
|
||||
( -- | The schema cache representation of the allowlist
|
||||
InlinedAllowlist,
|
||||
InlinedAllowlist (..),
|
||||
inlineAllowlist,
|
||||
AllowlistMode (..),
|
||||
allowlistAllowsQuery,
|
||||
@ -13,6 +13,7 @@ module Hasura.RQL.Types.Allowlist
|
||||
metadataAllowlistInsert,
|
||||
metadataAllowlistUpdateScope,
|
||||
metadataAllowlistAllCollections,
|
||||
NormalizedQuery (..),
|
||||
)
|
||||
where
|
||||
|
||||
|
@ -49,6 +49,7 @@ import Hasura.RQL.Types.Endpoint
|
||||
import Hasura.RQL.Types.EventTrigger
|
||||
import Hasura.RQL.Types.Instances ()
|
||||
import Hasura.RQL.Types.Permission
|
||||
import Hasura.RQL.Types.QueryCollection (CollectionName, ListedQuery (_lqName))
|
||||
import Hasura.RQL.Types.RemoteSchema
|
||||
import Hasura.SQL.AnyBackend qualified as AB
|
||||
import Hasura.Session
|
||||
@ -95,6 +96,7 @@ data MetadataObjId
|
||||
| MOInheritedRole !RoleName
|
||||
| MOEndpoint !EndpointName
|
||||
| MOHostTlsAllowlist !String
|
||||
| MOQueryCollectionsQuery !CollectionName !ListedQuery
|
||||
deriving (Show, Eq, Generic)
|
||||
|
||||
$(makePrisms ''MetadataObjId)
|
||||
@ -115,6 +117,7 @@ moiTypeName = \case
|
||||
MOInheritedRole _ -> "inherited_role"
|
||||
MOEndpoint _ -> "rest_endpoint"
|
||||
MOHostTlsAllowlist _ -> "host_network_tls_allowlist"
|
||||
MOQueryCollectionsQuery _ _ -> "query_collections"
|
||||
where
|
||||
handleSourceObj :: forall b. SourceMetadataObjId b -> Text
|
||||
handleSourceObj = \case
|
||||
@ -147,6 +150,7 @@ moiName objectId =
|
||||
MOInheritedRole inheritedRoleName -> "inherited role " <> toTxt inheritedRoleName
|
||||
MOEndpoint name -> toTxt name
|
||||
MOHostTlsAllowlist hostTlsAllowlist -> T.pack hostTlsAllowlist
|
||||
MOQueryCollectionsQuery cName lq -> (toTxt . _lqName) lq <> " in " <> toTxt cName
|
||||
where
|
||||
handleSourceObj ::
|
||||
forall b.
|
||||
|
@ -53,7 +53,7 @@ newtype GQLQuery = GQLQuery {unGQLQuery :: G.ExecutableDocument G.Name}
|
||||
|
||||
newtype GQLQueryWithText
|
||||
= GQLQueryWithText (Text, GQLQuery)
|
||||
deriving (Show, Eq, Ord, NFData, Generic, Cacheable)
|
||||
deriving (Show, Eq, Ord, NFData, Generic, Cacheable, Hashable)
|
||||
|
||||
instance FromJSON GQLQueryWithText where
|
||||
parseJSON v@(String t) = GQLQueryWithText . (t,) <$> parseJSON v
|
||||
@ -78,6 +78,8 @@ instance NFData ListedQuery
|
||||
|
||||
instance Cacheable ListedQuery
|
||||
|
||||
instance Hashable ListedQuery
|
||||
|
||||
$(deriveJSON hasuraJSON ''ListedQuery)
|
||||
|
||||
newtype CollectionDef = CollectionDef
|
||||
|
@ -331,7 +331,8 @@ data SchemaCache = SchemaCache
|
||||
scMetricsConfig :: !MetricsConfig,
|
||||
scMetadataResourceVersion :: !(Maybe MetadataResourceVersion),
|
||||
scSetGraphqlIntrospectionOptions :: !SetGraphqlIntrospectionOptions,
|
||||
scTlsAllowlist :: ![TlsAllow]
|
||||
scTlsAllowlist :: ![TlsAllow],
|
||||
scQueryCollections :: !QueryCollections
|
||||
}
|
||||
|
||||
-- WARNING: this can only be used for debug purposes, as it loses all
|
||||
@ -355,7 +356,8 @@ instance ToJSON SchemaCache where
|
||||
"metrics_config" .= toJSON scMetricsConfig,
|
||||
"metadata_resource_version" .= toJSON scMetadataResourceVersion,
|
||||
"set_graphql_introspection_options" .= toJSON scSetGraphqlIntrospectionOptions,
|
||||
"tls_allowlist" .= toJSON scTlsAllowlist
|
||||
"tls_allowlist" .= toJSON scTlsAllowlist,
|
||||
"query_collection" .= toJSON scQueryCollections
|
||||
]
|
||||
|
||||
getAllRemoteSchemas :: SchemaCache -> [RemoteSchemaName]
|
||||
|
@ -24,7 +24,7 @@ module Hasura.RQL.Types.SchemaCache.Build
|
||||
buildSchemaCacheFor,
|
||||
buildSchemaCacheStrict,
|
||||
withNewInconsistentObjsCheck,
|
||||
getInconsistentRestQueries,
|
||||
getInconsistentQueryCollections,
|
||||
)
|
||||
where
|
||||
|
||||
@ -39,7 +39,6 @@ import Data.HashMap.Strict.InsOrd qualified as OMap
|
||||
import Data.HashMap.Strict.Multi qualified as MultiMap
|
||||
import Data.List qualified as L
|
||||
import Data.Sequence qualified as Seq
|
||||
import Data.Set qualified as S
|
||||
import Data.Text.Extended
|
||||
import Data.Text.NonEmpty (unNonEmptyText)
|
||||
import Data.Trie qualified as Trie
|
||||
@ -47,8 +46,9 @@ import Database.MSSQL.Transaction qualified as MSSQL
|
||||
import Database.PG.Query qualified as Q
|
||||
import Hasura.Backends.Postgres.Connection
|
||||
import Hasura.Base.Error
|
||||
import Hasura.GraphQL.Analyse (Analysis (Analysis, _aErrs, _aFields), FieldAnalysis (FieldAnalysis, _fErrs, _fFields), FieldDef, analyzeGraphqlQuery)
|
||||
import Hasura.GraphQL.Analyse
|
||||
import Hasura.Prelude
|
||||
import Hasura.RQL.Types.Allowlist (NormalizedQuery, unNormalizedQuery)
|
||||
import Hasura.RQL.Types.Common
|
||||
import Hasura.RQL.Types.Endpoint
|
||||
import Hasura.RQL.Types.Metadata
|
||||
@ -349,7 +349,7 @@ withNewInconsistentObjsCheck action = do
|
||||
|
||||
let diffInconsistentObjects = Map.difference `on` groupInconsistentMetadataById
|
||||
newInconsistentObjects =
|
||||
L.nub $ concatMap toList $ Map.elems (currentObjects `diffInconsistentObjects` originalObjects)
|
||||
hashNub $ concatMap toList $ Map.elems (currentObjects `diffInconsistentObjects` originalObjects)
|
||||
unless (null newInconsistentObjects) $
|
||||
throwError
|
||||
(err500 Unexpected "cannot continue due to newly found inconsistent metadata")
|
||||
@ -358,36 +358,61 @@ withNewInconsistentObjsCheck action = do
|
||||
|
||||
pure result
|
||||
|
||||
-- | getInconsistentRestQueries is a helper function that runs the
|
||||
-- static analysis over the saved queries for each endpoints and
|
||||
-- reports any inconsistenties with the current schema.
|
||||
getInconsistentRestQueries :: Maybe RemoteSchemaIntrospection -> EndpointTrie GQLQueryWithText -> (EndpointMetadata GQLQueryWithText -> MetadataObject) -> [InconsistentMetadata]
|
||||
getInconsistentRestQueries Nothing _ _ = []
|
||||
getInconsistentRestQueries (Just rs) tMap getMetaObj = map (\(o, t) -> InconsistentObject t Nothing o) fE
|
||||
-- | getInconsistentQueryCollections is a helper function that runs the
|
||||
-- static analysis over the saved queries and reports any inconsistenties
|
||||
-- with the current schema.
|
||||
getInconsistentQueryCollections ::
|
||||
Maybe RemoteSchemaIntrospection ->
|
||||
QueryCollections ->
|
||||
((CollectionName, ListedQuery) -> MetadataObject) ->
|
||||
EndpointTrie GQLQueryWithText ->
|
||||
[NormalizedQuery] ->
|
||||
[InconsistentMetadata]
|
||||
getInconsistentQueryCollections Nothing _ _ _ _ = []
|
||||
getInconsistentQueryCollections (Just rs) qcs lqToMetadataObj restEndpoints allowLst = map (\(o, t) -> InconsistentObject t Nothing o) fE
|
||||
where
|
||||
methodList = concatMap S.toList $ concatMap MultiMap.elems $ Trie.elems tMap
|
||||
endpoints = concatMap (\x -> map (x,) (mdDefinitions x)) methodList
|
||||
mdDefinitions :: EndpointMetadata GQLQueryWithText -> [G.ExecutableDefinition G.Name]
|
||||
mdDefinitions = G.getExecutableDefinitions . unGQLQuery . getGQLQuery . _edQuery . _ceDefinition
|
||||
fE = lefts $ map (validateQuery rs) endpoints
|
||||
zipLQwithDef :: (CollectionName, CreateCollection) -> [((CollectionName, ListedQuery), G.ExecutableDefinition G.Name)]
|
||||
zipLQwithDef (cName, cc) =
|
||||
let lqs = _cdQueries . _ccDefinition $ cc
|
||||
in concatMap (\lq -> map ((cName, lq),) (G.getExecutableDefinitions . unGQLQuery . getGQLQuery . _lqQuery $ lq)) lqs
|
||||
inAllowList :: [NormalizedQuery] -> (ListedQuery) -> Bool
|
||||
inAllowList nqList (ListedQuery {..}) = any (\nq -> unNormalizedQuery nq == (unGQLQuery . getGQLQuery) _lqQuery) nqList
|
||||
|
||||
showErrLst = dquoteList . reverse
|
||||
formatError (endpointName, analysisErrs) = endpointName <> " (" <> showErrLst analysisErrs <> ")"
|
||||
validateQuery ::
|
||||
RemoteSchemaIntrospection ->
|
||||
(EndpointMetadata GQLQueryWithText, G.ExecutableDefinition G.Name) ->
|
||||
Either (MetadataObject, Text) ()
|
||||
validateQuery rSchema (eMeta, eDef) = do
|
||||
let analysis = analyzeGraphqlQuery eDef rSchema
|
||||
endpointName = unNonEmptyText $ unEndpointName (_ceName eMeta)
|
||||
case analysis of
|
||||
Nothing -> Left (getMetaObj eMeta, "Cannot analyse the GraphQL query for the REST endpoint: " <> endpointName)
|
||||
Just Analysis {..} ->
|
||||
let getFieldErrs :: [(G.Name, (FieldDef, Maybe (FieldAnalysis G.Name)))] -> [Text] -> [Text]
|
||||
getFieldErrs [] lst = lst
|
||||
getFieldErrs ((_, (_, Just FieldAnalysis {..})) : xs) lst = _fErrs <> (getFieldErrs (OMap.toList _fFields) []) <> (getFieldErrs xs lst)
|
||||
getFieldErrs ((_, (_, Nothing)) : xs) lst = getFieldErrs xs lst
|
||||
allErrs = _aErrs <> getFieldErrs (OMap.toList _aFields) []
|
||||
in if (null allErrs)
|
||||
then Right ()
|
||||
else Left (getMetaObj eMeta, formatError (endpointName, allErrs))
|
||||
inRESTEndpoints :: EndpointTrie GQLQueryWithText -> (ListedQuery) -> [Text]
|
||||
inRESTEndpoints edTrie lq = map fst $ filter (queryIsFaulty) allQueries
|
||||
where
|
||||
methodMaps = Trie.elems edTrie
|
||||
endpoints = concatMap snd $ concatMap (MultiMap.toList) methodMaps
|
||||
allQueries :: [(Text, GQLQueryWithText)]
|
||||
allQueries = map (\d -> (unNonEmptyText . unEndpointName . _ceName $ d, _edQuery . _ceDefinition $ d)) endpoints
|
||||
|
||||
queryIsFaulty :: (Text, GQLQueryWithText) -> Bool
|
||||
queryIsFaulty (_, gqlQ) = (_lqQuery lq) == gqlQ
|
||||
|
||||
lqLst = concatMap zipLQwithDef (OMap.toList qcs)
|
||||
fE = lefts $ map (validateQuery rs (lqToMetadataObj) formatError) lqLst
|
||||
formatError (cName, lq) allErrs =
|
||||
let msgInit = "In query collection \"" <> toTxt cName <> "\" the query \"" <> (toTxt . _lqName) lq <> "\" is invalid with the following error(s): "
|
||||
lToTxt = dquoteList . reverse
|
||||
faultyEndpoints = case inRESTEndpoints restEndpoints lq of
|
||||
[] -> ""
|
||||
ePoints -> ". This query is being used in the following REST endpoint(s): " <> lToTxt ePoints
|
||||
|
||||
isInAllowList = if inAllowList allowLst lq then ". This query is in allowlist." else ""
|
||||
in msgInit <> lToTxt allErrs <> faultyEndpoints <> isInAllowList
|
||||
|
||||
validateQuery ::
|
||||
RemoteSchemaIntrospection ->
|
||||
(a -> MetadataObject) ->
|
||||
(a -> [Text] -> Text) ->
|
||||
(a, G.ExecutableDefinition G.Name) ->
|
||||
Either (MetadataObject, Text) ()
|
||||
validateQuery rSchema getMetaObj formatError (eMeta, eDef) = do
|
||||
let analysis = analyzeGraphqlQuery eDef rSchema
|
||||
case analysis of
|
||||
Nothing -> Left (getMetaObj eMeta, formatError eMeta ["Cannot analyse the GraphQL query"])
|
||||
Just a ->
|
||||
let allErrs = getAllAnalysisErrs a
|
||||
in if (null allErrs)
|
||||
then Right ()
|
||||
else Left (getMetaObj eMeta, formatError eMeta allErrs)
|
||||
|
@ -26,7 +26,7 @@ import Data.Text qualified as T
|
||||
import Data.Text.Extended (commaSeparated)
|
||||
import Data.Text.NonEmpty
|
||||
import Data.Trie qualified as Trie
|
||||
import Hasura.GraphQL.Analyse (Analysis (Analysis, _aFields, _aVars), FieldAnalysis (FieldAnalysis, _fFields), FieldDef (FieldInfo, FieldList), analyzeGraphqlQuery)
|
||||
import Hasura.GraphQL.Analyse
|
||||
import Hasura.GraphQL.RemoteServer (getSchemaIntrospection)
|
||||
import Hasura.Prelude hiding (get, put)
|
||||
import Hasura.RQL.Types.Endpoint
|
||||
@ -43,7 +43,8 @@ data EndpointData = EndpointData
|
||||
_edProperties :: InsOrdHashMap Text (Referenced Schema),
|
||||
_edResponse :: Maybe Response,
|
||||
_edDescription :: Text, -- contains API comments and graphql query
|
||||
_edName :: Text
|
||||
_edName :: Text,
|
||||
_edErrs :: [Text]
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
@ -427,6 +428,7 @@ extractEndpointInfo sd method d = do
|
||||
_edDescription = getComment d
|
||||
_edName = unNonEmptyText $ unEndpointName $ _ceName d
|
||||
_edMethod = [unEndpointMethod method] -- NOTE: Methods are grouped with into matching endpoints - Name used for grouping.
|
||||
_edErrs = getAllAnalysisErrs _analysis
|
||||
|
||||
getEndpointsData :: Maybe RemoteSchemaIntrospection -> SchemaCache -> Declare (Definitions Schema) [EndpointData]
|
||||
getEndpointsData sd sc = do
|
||||
@ -553,8 +555,19 @@ declareOpenApiSpec sc = do
|
||||
|
||||
openAPIPaths = mkOpenAPISchema endpointLst
|
||||
|
||||
allWarnings = foldl addEndpointWarnings warnings endpointLst
|
||||
addEndpointWarnings :: Text -> EndpointData -> Text
|
||||
addEndpointWarnings oldWarn EndpointData {..} =
|
||||
if null _edErrs
|
||||
then oldWarn
|
||||
else
|
||||
oldWarn <> "\n\nEndpoint \""
|
||||
<> _edName
|
||||
<> "\":\n"
|
||||
<> foldl (\w err -> w <> "\n- ⚠️ " <> err) "" _edErrs
|
||||
|
||||
return $
|
||||
mempty
|
||||
& paths .~ openAPIPaths
|
||||
& info . title .~ "Rest Endpoints"
|
||||
& info . description ?~ "This OpenAPI specification is automatically generated by Hasura." <> warnings
|
||||
& info . description ?~ "This OpenAPI specification is automatically generated by Hasura." <> allWarnings
|
||||
|
@ -72,20 +72,15 @@
|
||||
response:
|
||||
internal:
|
||||
- definition:
|
||||
definition:
|
||||
query:
|
||||
collection_name: temp_collection
|
||||
query_name: temp_query
|
||||
url: temp_rest_API
|
||||
methods:
|
||||
- GET
|
||||
name: temp_rest_API
|
||||
comment:
|
||||
reason: "Inconsistent object: temp_rest_API (\"Couldn't find field temp_table\
|
||||
\ in root field query_root\")"
|
||||
name: rest_endpoint temp_rest_API
|
||||
type: rest_endpoint
|
||||
path: "$.args"
|
||||
name: temp_query
|
||||
query: query { temp_table { col1 col2 } }
|
||||
reason: "Inconsistent object: In query collection \"temp_collection\" the query\
|
||||
\ \"temp_query\" is invalid with the following error(s): \"Couldn't find field temp_table\
|
||||
\ in root field query_root\". This query is being used in the following REST endpoint(s):\
|
||||
\ \"temp_rest_API\""
|
||||
name: query_collections temp_query in temp_collection
|
||||
type: query_collections
|
||||
path: $.args
|
||||
error: cannot continue due to newly found inconsistent metadata
|
||||
code: unexpected
|
||||
|
||||
@ -93,20 +88,47 @@
|
||||
url: /v1/query
|
||||
status: 200
|
||||
query:
|
||||
type: bulk
|
||||
type: drop_rest_endpoint
|
||||
args:
|
||||
- type: drop_rest_endpoint
|
||||
args:
|
||||
name: temp_rest_API
|
||||
- type: drop_query_collection
|
||||
args:
|
||||
collection: temp_collection
|
||||
cascade: false
|
||||
name: temp_rest_API
|
||||
response:
|
||||
- message: success
|
||||
- message: success
|
||||
message: success
|
||||
|
||||
- description: cleanup 2 (drop the table)
|
||||
- description: untrack the table
|
||||
url: /v1/query
|
||||
status: 500
|
||||
query:
|
||||
type: untrack_table
|
||||
args:
|
||||
table:
|
||||
schema: public
|
||||
name: temp_table
|
||||
response:
|
||||
internal:
|
||||
- definition:
|
||||
name: temp_query
|
||||
query: query { temp_table { col1 col2 } }
|
||||
reason: "Inconsistent object: In query collection \"temp_collection\" the query\
|
||||
\ \"temp_query\" is invalid with the following error(s): \"Couldn't find field\
|
||||
\ temp_table in root field query_root\""
|
||||
name: query_collections temp_query in temp_collection
|
||||
type: query_collections
|
||||
path: $.args
|
||||
error: cannot continue due to newly found inconsistent metadata
|
||||
code: unexpected
|
||||
|
||||
- description: cleanup 2 (drop the query collection)
|
||||
url: /v1/query
|
||||
status: 200
|
||||
query:
|
||||
type: drop_query_collection
|
||||
args:
|
||||
collection: temp_collection
|
||||
cascade: true
|
||||
response:
|
||||
message: success
|
||||
|
||||
- description: cleanup 3 (drop the table)
|
||||
url: /v1/query
|
||||
status: 200
|
||||
query:
|
||||
|
@ -1,25 +1,5 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: create_query_collection
|
||||
args:
|
||||
name: test_collection
|
||||
definition:
|
||||
queries:
|
||||
- name: simple_query
|
||||
query: "query { test_table { first_name last_name } }"
|
||||
- name: simple_query_cached
|
||||
query: "query @cached(ttl: 5) { test_table { first_name last_name } }"
|
||||
- name: query_with_arg
|
||||
query: "query ($first_name:String!) { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
|
||||
- name: query_with_args
|
||||
query: "query ($first_name: String!, $last_name:String!) { test_table(where: {first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) { first_name last_name } }"
|
||||
- name: query_with_uuid_arg
|
||||
query: "query ($id: uuid!) { test_table(where: {id: { _neq: $id }}) { first_name last_name } }"
|
||||
- name: query_with_uuid_args
|
||||
query: "query ($ids: [uuid!]!) { test_table(where: {id: { _in: $ids }}) { first_name last_name } }"
|
||||
- name: simple_subscription
|
||||
query: "subscription { test_table { first_name last_name } }"
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
@ -43,6 +23,26 @@ args:
|
||||
('Baz', 'Qux'),
|
||||
('X%20Y', 'Test');
|
||||
|
||||
- type: create_query_collection
|
||||
args:
|
||||
name: test_collection
|
||||
definition:
|
||||
queries:
|
||||
- name: simple_query
|
||||
query: "query { test_table { first_name last_name } }"
|
||||
- name: simple_query_cached
|
||||
query: "query @cached(ttl: 5) { test_table { first_name last_name } }"
|
||||
- name: query_with_arg
|
||||
query: "query ($first_name:String!) { test_table(where: {first_name: { _eq: $first_name } }) { first_name last_name } }"
|
||||
- name: query_with_args
|
||||
query: "query ($first_name: String!, $last_name:String!) { test_table(where: {first_name: { _eq: $first_name } last_name: { _eq: $last_name }}) { first_name last_name } }"
|
||||
- name: query_with_uuid_arg
|
||||
query: "query ($id: uuid!) { test_table(where: {id: { _neq: $id }}) { first_name last_name } }"
|
||||
- name: query_with_uuid_args
|
||||
query: "query ($ids: [uuid!]!) { test_table(where: {id: { _in: $ids }}) { first_name last_name } }"
|
||||
- name: simple_subscription
|
||||
query: "subscription { test_table { first_name last_name } }"
|
||||
|
||||
- type: create_rest_endpoint
|
||||
args:
|
||||
url: simple
|
||||
|
@ -5,6 +5,11 @@ args:
|
||||
collection: collection_1
|
||||
cascade: True
|
||||
|
||||
- type: drop_query_collection
|
||||
args:
|
||||
collection: collection_2
|
||||
cascade: True
|
||||
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
|
@ -0,0 +1,40 @@
|
||||
- description: Call openapi json endpoint
|
||||
url: /api/swagger/json
|
||||
method: GET
|
||||
status: 200
|
||||
query:
|
||||
response:
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
version: ''
|
||||
title: Rest Endpoints
|
||||
description: "This OpenAPI specification is automatically generated by Hasura.\n\
|
||||
\nEndpoint \"wrong_endpoint\":\n\n- ⚠️ Couldn't find field random_field_name\
|
||||
\ in root field query_root\n- ⚠️ Couldn't find definition for field random_col_name\
|
||||
\ in test_table"
|
||||
paths:
|
||||
/api/rest/some_url:
|
||||
get:
|
||||
summary: wrong_endpoint
|
||||
description: "***\nThe GraphQl query for this endpoint is:\n``` graphql\n\
|
||||
query { random_field_name test_table { random_col_name } }\n```"
|
||||
parameters:
|
||||
- schema:
|
||||
type: string
|
||||
in: header
|
||||
name: x-hasura-admin-secret
|
||||
description: Your x-hasura-admin-secret will be used for authentication
|
||||
of the API request.
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
test_table:
|
||||
items:
|
||||
type: object
|
||||
type: array
|
||||
nullable: false
|
||||
description: Responses for GET /api/rest/some_url
|
||||
components: {}
|
@ -163,8 +163,8 @@ args:
|
||||
- name: query_2
|
||||
query: |
|
||||
query {
|
||||
test2{
|
||||
id
|
||||
test2_select{
|
||||
test2_id
|
||||
}
|
||||
}
|
||||
|
||||
@ -184,7 +184,7 @@ args:
|
||||
query: |
|
||||
query {
|
||||
get_test2(args: {id: 1}){
|
||||
id
|
||||
test2_id
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,13 @@
|
||||
type: bulk
|
||||
args:
|
||||
- type: drop_query_collection
|
||||
args:
|
||||
collection: collection_1
|
||||
cascade: true
|
||||
- type: drop_query_collection
|
||||
args:
|
||||
collection: collection_2
|
||||
cascade: true
|
||||
- type: run_sql
|
||||
args:
|
||||
sql: |
|
||||
|
@ -1,3 +1,4 @@
|
||||
import collections
|
||||
import pytest
|
||||
import os
|
||||
from validate import check_query_f, check_query, get_conf_f
|
||||
@ -52,3 +53,65 @@ class TestOpenAPISpec:
|
||||
|
||||
def test_duplicate_field_name(self, hge_ctx, transport):
|
||||
check_query_f(hge_ctx, self.dir() + '/openapi_get_endpoint_test_duplicate_field_name.yaml', transport)
|
||||
|
||||
def test_inconsistent_schema_openAPI(self, hge_ctx, transport):
|
||||
# export metadata and create a backup
|
||||
st_code, backup_metadata = hge_ctx.v1q(
|
||||
q = {
|
||||
"type": "export_metadata",
|
||||
"args": {}
|
||||
}
|
||||
)
|
||||
assert st_code == 200, backup_metadata
|
||||
|
||||
new_metadata = backup_metadata.copy()
|
||||
|
||||
#create inconsistent metadata
|
||||
inconsistent_query = [collections.OrderedDict([
|
||||
('name', 'wrong_queries'),
|
||||
('definition', collections.OrderedDict([
|
||||
('queries', [collections.OrderedDict([
|
||||
('name', 'random_query'),
|
||||
('query', 'query { random_field_name test_table { random_col_name } }')
|
||||
])])
|
||||
]))
|
||||
])]
|
||||
res_endpoint = [collections.OrderedDict([
|
||||
("definition", collections.OrderedDict([
|
||||
("query", collections.OrderedDict([
|
||||
("collection_name", "wrong_queries"),
|
||||
("query_name", "random_query")
|
||||
]))
|
||||
])),
|
||||
("url", "some_url"),
|
||||
("methods", ["GET"]),
|
||||
("name", "wrong_endpoint"),
|
||||
("comment", None)
|
||||
])]
|
||||
new_metadata["query_collections"] = inconsistent_query
|
||||
new_metadata["rest_endpoints"] = res_endpoint
|
||||
|
||||
# apply inconsistent metadata
|
||||
st_code, resp = hge_ctx.v1q(
|
||||
q={
|
||||
"type": "replace_metadata",
|
||||
"version": 2,
|
||||
"args": {
|
||||
"allow_inconsistent_metadata": True,
|
||||
"metadata": new_metadata
|
||||
}
|
||||
}
|
||||
)
|
||||
assert st_code == 200, resp
|
||||
|
||||
# check openAPI schema
|
||||
check_query_f(hge_ctx, self.dir() + '/openapi_inconsistent_schema.yaml', transport)
|
||||
|
||||
# revert to old metadata
|
||||
st_code, resp = hge_ctx.v1q(
|
||||
q={
|
||||
"type": "replace_metadata",
|
||||
"args": backup_metadata
|
||||
}
|
||||
)
|
||||
assert st_code == 200, resp
|
||||
|
Loading…
Reference in New Issue
Block a user