graphql-engine/server/src-lib/Hasura/GraphQL/ParameterizedQueryHash.hs
2021-08-02 16:05:17 +00:00

185 lines
7.0 KiB
Haskell

{-|
This module calculates parameterized query hash, which is a way to
hash an incoming query (after resolving variables) with all leaf nodes
(i.e. scalar values) discarded. In other words, two queries having the same
parameterized query hash are essentially the same query but may differ in
leaf values.
For example:
1. query {
authors (where: {id: {_eq: 2}}) {
id
name
}
}
2. query {
authors (where: {id: {_eq: 203943}}) {
id
name
}
}
3. query {
authors (where: {id: {_eq: $id}}) {
id
name
}
}
For any value of `id`
4. query {
authors (where: $whereBoolExp) {
id
name
}
}
only when `whereBoolExp` is of the form of
{
"id": {
"_eq": <id>
}
}
All the above queries should result in the same parameterized query hash.
The following steps are done to calculate the parameterized query hash:
1. Normalize the GraphQL query by substituting the variables (if any) in appropriate places.
2. Substitute any scalar GraphQL values (Int, Float, Enum, String and Boolean) to null
3. For input objects and list, traverse through them and do step no 2.
4. Calculate the hash of the query obtained from step 3.
Note: Parameterized query hash is a PRO only feature
-}
module Hasura.GraphQL.ParameterizedQueryHash
( calculateParameterizedQueryHash
, mkUnsafeParameterizedQueryHash
, unParamQueryHash
, ParameterizedQueryHash
, ParameterizedQueryHashList(..)
,parameterizedQueryHashListToObject)
where
import Hasura.Prelude
import qualified Data.Aeson as J
import qualified Data.ByteString as B
import qualified Data.HashMap.Strict as Map
import qualified Language.GraphQL.Draft.Printer as G
import qualified Language.GraphQL.Draft.Syntax as G
import qualified Text.Builder as Text
import Hasura.GraphQL.Parser (InputValue (..), Variable (..))
import Hasura.Server.Utils (cryptoHash)
-- | a set of parameterized query hashes attached to a request
-- this type exists because a simple list of 'ParameterisedQueryHash'es won't
-- let us log a single-request batch and a single non-batched request
-- differently. the log format uses json lists for requests executed in batched
-- mode, for fields like @query@, but not for requests in single mode (e.g.
-- @query: "..."@ vs @query: ["..."]@) and so to conform to that, we capture the
-- whole _set_ of parameterised query hashes when it's created, tagging it with
-- information about how it was created (i.e. from a batched request, a single
-- request, etc.)
data ParameterizedQueryHashList
= PQHSetEmpty
-- ^ an empty query hash set, either for an operation that does not produce
-- query hashes, or due to failure in operation execution
| PQHSetSingleton !ParameterizedQueryHash
-- ^ a query hash set consisting of a single element, corresponding to e.g.
-- a single (non-batched) graphql request
| PQHSetBatched ![ParameterizedQueryHash]
-- ^ a query hash set associated to a batched request
-- note that this does not need to contain multiple query hashes: it is possible
-- for a batch to contain only one request
deriving (Show, Eq)
-- we use something that explicitly produces an 'J.Object' instead of writing
-- a 'J.ToJSON' instance. in the latter case, functions consuming the output of
-- 'J.toJSON' would have to perform a partial pattern-match on the 'J.Value'
-- output to extract a JSON object from it. for the other patterns, it would
-- have to either throw a runtime error on or silently ignore the other
-- patterns, and the latter choice would cause a silent failure if the
-- 'J.ToJSON' instance were modified to no longer always return objects
parameterizedQueryHashListToObject :: ParameterizedQueryHashList -> J.Object
parameterizedQueryHashListToObject = Map.fromList . \case
-- when a non-graphql query is executed, or when the request fails,
-- there are no hashes to log
PQHSetEmpty -> []
-- when there's no batching of graphql queries, we log the parameterized query hash as a string
PQHSetSingleton queryHash ->
[("parameterized_query_hash" , J.toJSON queryHash)]
-- when there's a batch of graphql queries (even if the batch contains only one request),
-- we log the parameterized query hashes of every request in a list
PQHSetBatched queryHashes ->
[("parameterized_query_hash" , J.toJSON queryHashes)]
newtype ParameterizedQueryHash
= ParameterizedQueryHash { unParamQueryHash :: B.ByteString }
deriving (Show, Eq)
instance J.ToJSON ParameterizedQueryHash where
toJSON = J.String . bsToTxt . unParamQueryHash
normalizeSelectionSet :: G.SelectionSet G.NoFragments Variable -> G.SelectionSet G.NoFragments Void
normalizeSelectionSet = (normalizeSelection =<<)
where
normalizeSelection :: G.Selection G.NoFragments Variable -> G.SelectionSet G.NoFragments Void
normalizeSelection (G.SelectionField fld) = pure $ G.SelectionField (normalizeField fld)
normalizeSelection (G.SelectionInlineFragment (G.InlineFragment _ _ selSet)) =
normalizeSelectionSet selSet
normalizeField (G.Field _alias name args _directives selSet) =
G.Field Nothing name (Map.map normalizeValue args) mempty $ normalizeSelectionSet selSet
normalizeConstValue :: G.Value Void -> G.Value Void
normalizeConstValue = \case
G.VNull -> G.VNull
G.VInt _ -> G.VNull
G.VFloat _ -> G.VNull
G.VString _ -> G.VNull
G.VBoolean _ -> G.VNull
G.VEnum _ -> G.VNull
G.VList l -> G.VList $ map normalizeConstValue l
G.VObject obj -> G.VObject $ Map.map normalizeConstValue obj
jsonToNormalizedGQLVal :: J.Value -> G.Value Void
jsonToNormalizedGQLVal = \case
J.Null -> G.VNull
J.Bool _ -> G.VNull
J.String _ -> G.VNull
J.Number _ -> G.VNull
J.Array l -> G.VList $ jsonToNormalizedGQLVal <$> toList l
J.Object vals -> G.VObject $ Map.fromList $
flip map (Map.toList vals) $ \(key, val) ->
(G.unsafeMkName key, jsonToNormalizedGQLVal val)
normalizeValue :: G.Value Variable -> G.Value Void
normalizeValue = \case
G.VNull -> G.VNull
G.VInt _ -> G.VNull
G.VFloat _ -> G.VNull
G.VString _ -> G.VNull
G.VBoolean _ -> G.VNull
G.VEnum _ -> G.VNull
G.VList l -> G.VList $ map normalizeValue l
G.VObject obj -> G.VObject $ Map.map normalizeValue obj
G.VVariable (Variable _info _type value) ->
case value of
GraphQLValue val -> normalizeConstValue val
JSONValue v -> jsonToNormalizedGQLVal v
calculateParameterizedQueryHash :: G.SelectionSet G.NoFragments Variable -> ParameterizedQueryHash
calculateParameterizedQueryHash = ParameterizedQueryHash . cryptoHash . Text.run . G.selectionSet . normalizeSelectionSet
mkUnsafeParameterizedQueryHash :: Text -> ParameterizedQueryHash
mkUnsafeParameterizedQueryHash = ParameterizedQueryHash . txtToBs