graphql-engine/server/src-dc-api/Hasura/Backends/DataConnector/API/V0/Query.hs
David Overton f4419236ed Gdc capabilities scalar types - GDW-87
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5597
GitOrigin-RevId: 4f561bf476266955d7b1d3dbca4d406a97bf8b34
2022-09-06 04:26:03 +00:00

237 lines
9.9 KiB
Haskell

{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE TemplateHaskell #-}
module Hasura.Backends.DataConnector.API.V0.Query
( QueryRequest (..),
qrTable,
qrTableRelationships,
qrQuery,
Query (..),
qFields,
qAggregates,
qLimit,
qOffset,
qWhere,
qOrderBy,
Field (..),
RelationshipField (..),
QueryResponse (..),
qrRows,
qrAggregates,
FieldValue,
mkColumnFieldValue,
mkRelationshipFieldValue,
deserializeAsColumnFieldValue,
deserializeAsRelationshipFieldValue,
_ColumnFieldValue,
_RelationshipFieldValue,
)
where
import Autodocodec.Extended
import Autodocodec.OpenAPI ()
import Control.Arrow (left)
import Control.Lens (Lens', Prism', lens, prism')
import Control.Lens.TH (makeLenses, makePrisms)
import Data.Aeson (FromJSON, ToJSON, Value)
import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as KM
import Data.Data (Data)
import Data.HashMap.Strict qualified as HashMap
import Data.OpenApi (ToSchema)
import Data.Text (Text)
import Data.Text qualified as T
import GHC.Generics (Generic)
import GHC.Show (appPrec, appPrec1)
import Hasura.Backends.DataConnector.API.V0.Aggregate qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Column qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Expression qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.OrderBy qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Relationships qualified as API.V0
import Hasura.Backends.DataConnector.API.V0.Table qualified as API.V0
import Prelude
-- | A serializable request to retrieve strutured data from some
-- source.
data QueryRequest = QueryRequest
{ _qrTable :: API.V0.TableName,
_qrTableRelationships :: [API.V0.TableRelationships],
_qrQuery :: Query
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec QueryRequest
instance HasCodec QueryRequest where
codec =
object "QueryRequest" $
QueryRequest
<$> requiredField "table" "The name of the table to query" .= _qrTable
<*> requiredField "table_relationships" "The relationships between tables involved in the entire query request" .= _qrTableRelationships
<*> requiredField "query" "The details of the query against the table" .= _qrQuery
-- | The details of a query against a table
data Query = Query
{ _qFields :: Maybe (KM.KeyMap Field),
_qAggregates :: Maybe (KM.KeyMap API.V0.Aggregate),
_qLimit :: Maybe Int,
_qOffset :: Maybe Int,
_qWhere :: Maybe API.V0.Expression,
_qOrderBy :: Maybe API.V0.OrderBy
}
deriving stock (Eq, Ord, Show, Generic, Data)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Query
instance HasCodec Query where
codec =
named "Query" . object "Query" $
Query
<$> optionalFieldOrNull "fields" "Fields of the query" .= _qFields
<*> optionalFieldOrNull "aggregates" "Aggregate fields of the query" .= _qAggregates
<*> optionalFieldOrNull "limit" "Optionally limit to N results" .= _qLimit
<*> optionalFieldOrNull "offset" "Optionally offset from the Nth result" .= _qOffset
<*> optionalFieldOrNull "where" "Optionally constrain the results to satisfy some predicate" .= _qWhere
<*> optionalFieldOrNull "order_by" "Optionally order the results by the value of one or more fields" .= _qOrderBy
data RelationshipField = RelationshipField
{ _rfRelationship :: API.V0.RelationshipName,
_rfQuery :: Query
}
deriving stock (Eq, Ord, Show, Generic, Data)
relationshipFieldObjectCodec :: JSONObjectCodec RelationshipField
relationshipFieldObjectCodec =
RelationshipField
<$> requiredField "relationship" "The name of the relationship to follow for the subquery" .= _rfRelationship
<*> requiredField "query" "Relationship query" .= _rfQuery
-- | A serializable field targeted by a 'Query'.
data Field
= ColumnField API.V0.ColumnName
| RelField RelationshipField
deriving stock (Eq, Ord, Show, Generic, Data)
deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Field
instance HasCodec Field where
codec =
named "Field" $
object "Field" $
discriminatedUnionCodec "type" enc dec
where
columnCodec = requiredField' "column"
enc = \case
ColumnField columnName -> ("column", mapToEncoder columnName columnCodec)
RelField relField -> ("relationship", mapToEncoder relField relationshipFieldObjectCodec)
dec =
HashMap.fromList
[ ("column", ("ColumnField", mapToDecoder ColumnField columnCodec)),
("relationship", ("RelationshipField", mapToDecoder RelField relationshipFieldObjectCodec))
]
-- | The resolved query response provided by the 'POST /query'
-- endpoint encoded as a list of JSON objects.
data QueryResponse = QueryResponse
{ _qrRows :: Maybe [KM.KeyMap FieldValue],
_qrAggregates :: Maybe (KM.KeyMap Value)
}
deriving stock (Eq, Ord, Show)
deriving (ToJSON, FromJSON, ToSchema) via Autodocodec QueryResponse
instance HasCodec QueryResponse where
codec =
named "QueryResponse" . object "QueryResponse" $
QueryResponse
<$> optionalFieldOrNull "rows" "The rows returned by the query, corresponding to the query's fields" .= _qrRows
<*> optionalFieldOrNull "aggregates" "The results of the aggregates returned by the query" .= _qrAggregates
-- | FieldValue represents the value of a field in a 'QueryResponse', which in reality can
-- be two things. One, a column field value which can be any JSON 'J.Value', or two, a
-- relationship field value which can be a 'QueryResponse'.
--
-- Unfortunately, QueryResponse overlaps entirely with 'J.Value', so it is not possible
-- without additional context to know whether a 'FieldValue' is a column field value or a
-- relationship field value. That additional context is the original query, in which
-- we describe whether we expect a field value to be a relationship or a column.
--
-- Why did we allow entirely overlapping types and not have a discriminator? Because it
-- simplifies agent implementation to not have to inject discriminators into the response
-- as it is generated. We have chosen to make this complicated to deal with in HGE as a
-- tradeoff against simplifying agent implementation.
--
-- The 'Eq', 'Ord' and 'Show' instances try to guess whether there is a relationship
-- field value or a column field value by assuming that if the JSON can be deserialized
-- into a 'QueryResponse', then it must be a relationship field value. (This is not
-- strictly true, since an agent could theoretically make a custom field type that could
-- look like a 'QueryResponse'). We do this kludge because when comparing relationship
-- field values (mainly for testing purposes), we must compare them using 'QueryResponse',
-- since raw JSON comparisons will show up immaterial differences between null properties
-- and missing properties (which we consider to be the same thing here).
newtype FieldValue = FieldValue J.Value
deriving (ToJSON, FromJSON, ToSchema) via Autodocodec FieldValue
instance Eq FieldValue where
l == r = deserializeFieldValueByGuessing l == deserializeFieldValueByGuessing r
instance Ord FieldValue where
l <= r = deserializeFieldValueByGuessing l <= deserializeFieldValueByGuessing r
instance Show FieldValue where
showsPrec d fieldValue =
case deserializeFieldValueByGuessing fieldValue of
Left columnFieldValue -> showParen (d > appPrec) $ showString "ColumnFieldValue " . showsPrec appPrec1 columnFieldValue
Right queryResponse -> showParen (d > appPrec) $ showString "RelationshipFieldValue " . showsPrec appPrec1 queryResponse
mkColumnFieldValue :: J.Value -> FieldValue
mkColumnFieldValue = FieldValue
mkRelationshipFieldValue :: QueryResponse -> FieldValue
mkRelationshipFieldValue = FieldValue . J.toJSON
deserializeAsColumnFieldValue :: FieldValue -> J.Value
deserializeAsColumnFieldValue (FieldValue value) = value
deserializeAsRelationshipFieldValue :: FieldValue -> Either Text QueryResponse
deserializeAsRelationshipFieldValue (FieldValue value) =
case J.fromJSON value of
J.Error s -> Left $ T.pack s
J.Success queryResponse -> Right queryResponse
deserializeFieldValueByGuessing :: FieldValue -> Either J.Value QueryResponse
deserializeFieldValueByGuessing fieldValue =
left (const $ deserializeAsColumnFieldValue fieldValue) $ deserializeAsRelationshipFieldValue fieldValue
-- | Even though we could just describe a FieldValue as "any JSON value", we're explicitly
-- describing it in terms of either a 'QueryResponse' or "any JSON value", in order to
-- make it clear what the options are explicitly.
instance HasCodec FieldValue where
codec =
dimapCodec encode decode $
possiblyJointEitherCodec anyJsonValueCodec (possiblyJointEitherCodec queryResponseCodec nullColumnFieldValue)
where
queryResponseCodec :: JSONCodec QueryResponse
queryResponseCodec = codec
anyJsonValueCodec :: JSONCodec J.Value
anyJsonValueCodec = named "ColumnFieldValue" valueCodec
-- We have to explicitly call out null as a separate named type in OpenAPI
-- to get the typescript type-generator to recognise null as a valid value here
nullColumnFieldValue :: JSONCodec ()
nullColumnFieldValue = named "NullColumnFieldValue" nullCodec
encode :: Either J.Value (Either QueryResponse ()) -> FieldValue
encode = FieldValue . either id (either J.toJSON (const J.Null))
decode :: FieldValue -> Either J.Value (Either QueryResponse ())
decode = Left . deserializeAsColumnFieldValue
_ColumnFieldValue :: Lens' FieldValue J.Value
_ColumnFieldValue = lens deserializeAsColumnFieldValue (const mkColumnFieldValue)
_RelationshipFieldValue :: Prism' FieldValue QueryResponse
_RelationshipFieldValue = prism' mkRelationshipFieldValue (either (const Nothing) Just . deserializeAsRelationshipFieldValue)
$(makeLenses ''QueryRequest)
$(makeLenses ''Query)
$(makeLenses ''QueryResponse)
$(makePrisms ''FieldValue)