graphql-engine/server/src-lib/Hasura/LogicalModel/Schema.hs
2023-03-02 16:04:18 +00:00

152 lines
5.7 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
-- | Schema parsers for logical models.
module Hasura.LogicalModel.Schema (defaultBuildLogicalModelRootFields) where
import Data.Has (Has (getter))
import Data.HashMap.Strict qualified as HM
import Data.Monoid (Ap (Ap, getAp))
import Hasura.GraphQL.Schema.Backend
( BackendCustomTypeSelectSchema (..),
BackendSchema (columnParser),
MonadBuildSchema,
)
import Hasura.GraphQL.Schema.Common
( SchemaT,
retrieve,
)
import Hasura.GraphQL.Schema.Options qualified as Options
import Hasura.GraphQL.Schema.Parser qualified as P
import Hasura.GraphQL.Schema.Select
( customTypeSelectionList,
)
import Hasura.LogicalModel.Cache (LogicalModelInfo (..))
import Hasura.LogicalModel.IR (LogicalModel (..))
import Hasura.LogicalModel.Metadata (InterpolatedQuery (..), LogicalModelArgumentName (getLogicalModelArgumentName))
import Hasura.LogicalModel.Types (getLogicalModelName)
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp (gBoolExpTrue)
import Hasura.RQL.IR.Root (RemoteRelationshipField)
import Hasura.RQL.IR.Select (QueryDB (QDBMultipleRows))
import Hasura.RQL.IR.Select qualified as IR
import Hasura.RQL.IR.Value (UnpreparedValue (UVParameter), openValueOrigin)
import Hasura.RQL.Types.Backend
( Backend (ScalarType),
)
import Hasura.RQL.Types.Column qualified as Column
import Hasura.RQL.Types.Metadata.Object qualified as MO
import Hasura.RQL.Types.Source
( SourceInfo (_siCustomization, _siName),
)
import Hasura.RQL.Types.SourceCustomization
( ResolvedSourceCustomization (_rscNamingConvention),
)
import Hasura.SQL.AnyBackend (mkAnyBackend)
import Language.GraphQL.Draft.Syntax qualified as G
import Language.GraphQL.Draft.Syntax.QQ qualified as G
defaultBuildLogicalModelRootFields ::
forall b r m n.
( MonadBuildSchema b r m n,
BackendCustomTypeSelectSchema b
) =>
LogicalModelInfo b ->
SchemaT
r
m
(Maybe (P.FieldParser n (QueryDB b (RemoteRelationshipField UnpreparedValue) (UnpreparedValue b))))
defaultBuildLogicalModelRootFields LogicalModelInfo {..} = runMaybeT $ do
let fieldName = getLogicalModelName _lmiRootFieldName
logicalModelArgsParser <- logicalModelArgumentsSchema @b @r @m @n fieldName _lmiArguments
sourceInfo :: SourceInfo b <- asks getter
let sourceName = _siName sourceInfo
tCase = _rscNamingConvention $ _siCustomization sourceInfo
description = G.Description <$> _lmiDescription
stringifyNumbers <- retrieve Options.soStringifyNumbers
selectionSetParser <- MaybeT $ customTypeSelectionList @b @r @m @n (getLogicalModelName _lmiRootFieldName) _lmiReturns
customTypesArgsParser <- lift $ customTypeArguments @b @r @m @n (getLogicalModelName _lmiRootFieldName) _lmiReturns
let interpolatedQuery lmArgs =
InterpolatedQuery $
(fmap . fmap)
( \var -> case HM.lookup var lmArgs of
Just arg -> UVParameter Nothing arg
Nothing ->
-- the `logicalModelArgsParser` will already have checked
-- we have all the args the query needs so this _should
-- not_ happen
error $ "No logical model arg passed for " <> show var
)
(getInterpolatedQuery _lmiCode)
pure $
P.setFieldParserOrigin (MO.MOSourceObjId sourceName (mkAnyBackend $ MO.SMOLogicalModel @b _lmiRootFieldName)) $
P.subselection
fieldName
description
( (,)
<$> customTypesArgsParser
<*> logicalModelArgsParser
)
selectionSetParser
<&> \((args, lmArgs), fields) ->
QDBMultipleRows $
IR.AnnSelectG
{ IR._asnFields = fields,
IR._asnFrom =
IR.FromLogicalModel
LogicalModel
{ lmRootFieldName = _lmiRootFieldName,
lmArgs,
lmInterpolatedQuery = interpolatedQuery lmArgs
},
IR._asnPerm = IR.TablePerm gBoolExpTrue Nothing,
IR._asnArgs = args,
IR._asnStrfyNum = stringifyNumbers,
IR._asnNamingConvention = Just tCase
}
logicalModelArgumentsSchema ::
forall b r m n.
MonadBuildSchema b r m n =>
G.Name ->
HashMap LogicalModelArgumentName (ScalarType b) ->
MaybeT (SchemaT r m) (P.InputFieldsParser n (HashMap LogicalModelArgumentName (Column.ColumnValue b)))
logicalModelArgumentsSchema logicalModelName argsSignature = do
-- Lift 'SchemaT r m (InputFieldsParser ..)' into a monoid using Applicative.
-- This lets us use 'foldMap' + monoid structure of hashmaps to avoid awkwardly
-- traversing the arguments and building the resulting parser.
argsParser <-
getAp $
foldMap
( \(name, ty) -> Ap do
argValueParser <-
fmap (HM.singleton name . openValueOrigin)
<$> lift (columnParser (Column.ColumnScalar ty) (G.Nullability False))
-- TODO: Break in some interesting way if we cannot make a name?
-- TODO: Naming conventions?
-- TODO: Custom fields? (Probably not)
argName <- hoistMaybe (G.mkName (getLogicalModelArgumentName name))
return $
P.field
argName
(Just $ G.Description ("Logical model argument " <> getLogicalModelArgumentName name))
argValueParser
)
(HM.toList argsSignature)
let desc = Just $ G.Description $ G.unName logicalModelName <> " Logical Model Arguments"
pure $
if null argsSignature
then mempty
else
P.field
[G.name|args|]
desc
(P.object (logicalModelName <> [G.name|_arguments|]) desc argsParser)