2023-04-19 12:03:36 +03:00
|
|
|
{-# LANGUAGE UndecidableInstances #-}
|
|
|
|
|
|
|
|
-- | Define and handle v1/metadata API operations to track, untrack, and get logical models.
|
|
|
|
module Hasura.LogicalModel.API
|
|
|
|
( GetLogicalModel (..),
|
|
|
|
TrackLogicalModel (..),
|
|
|
|
UntrackLogicalModel (..),
|
|
|
|
runGetLogicalModel,
|
2023-06-12 09:51:11 +03:00
|
|
|
execTrackLogicalModel,
|
|
|
|
execUntrackLogicalModel,
|
2023-04-19 12:03:36 +03:00
|
|
|
dropLogicalModelInMetadata,
|
|
|
|
CreateLogicalModelPermission (..),
|
|
|
|
DropLogicalModelPermission (..),
|
|
|
|
runCreateSelectLogicalModelPermission,
|
|
|
|
runDropSelectLogicalModelPermission,
|
|
|
|
module Hasura.LogicalModel.Types,
|
|
|
|
)
|
|
|
|
where
|
|
|
|
|
|
|
|
import Autodocodec (HasCodec)
|
|
|
|
import Autodocodec qualified as AC
|
2023-06-14 23:43:24 +03:00
|
|
|
import Control.Lens (Traversal', has, preview, (^?))
|
2023-04-19 12:03:36 +03:00
|
|
|
import Data.Aeson
|
2023-04-27 10:41:55 +03:00
|
|
|
import Data.HashMap.Strict.InsOrd.Extended qualified as InsOrdHashMap
|
2023-04-19 12:03:36 +03:00
|
|
|
import Data.Text.Extended (toTxt, (<<>))
|
|
|
|
import Hasura.Base.Error
|
|
|
|
import Hasura.EncJSON
|
2023-07-10 16:45:42 +03:00
|
|
|
import Hasura.LogicalModel.Lenses (lmmSelectPermissions)
|
|
|
|
import Hasura.LogicalModel.Metadata (LogicalModelMetadata (..))
|
2023-04-19 12:03:36 +03:00
|
|
|
import Hasura.LogicalModel.Types (LogicalModelField, LogicalModelName, logicalModelFieldMapCodec)
|
|
|
|
import Hasura.Prelude
|
|
|
|
import Hasura.RQL.Types.Backend (Backend (..))
|
2023-06-05 18:15:17 +03:00
|
|
|
import Hasura.RQL.Types.BackendTag (backendPrefix, backendTag, reify)
|
2023-04-24 21:35:48 +03:00
|
|
|
import Hasura.RQL.Types.BackendType
|
2023-04-19 12:03:36 +03:00
|
|
|
import Hasura.RQL.Types.Common (SourceName, defaultSource, sourceNameToText, successMsg)
|
|
|
|
import Hasura.RQL.Types.Metadata
|
|
|
|
import Hasura.RQL.Types.Metadata.Backend
|
|
|
|
import Hasura.RQL.Types.Metadata.Object
|
|
|
|
import Hasura.RQL.Types.Permission (PermDef (_pdRole), SelPerm)
|
2023-04-24 11:50:29 +03:00
|
|
|
import Hasura.RQL.Types.Roles (RoleName)
|
2023-04-19 12:03:36 +03:00
|
|
|
import Hasura.RQL.Types.SchemaCache.Build
|
|
|
|
import Hasura.SQL.AnyBackend qualified as AB
|
|
|
|
|
|
|
|
-- | Default implementation of the 'track_logical_model' request payload.
|
|
|
|
data TrackLogicalModel (b :: BackendType) = TrackLogicalModel
|
|
|
|
{ tlmSource :: SourceName,
|
|
|
|
tlmName :: LogicalModelName,
|
|
|
|
tlmDescription :: Maybe Text,
|
2023-04-27 10:41:55 +03:00
|
|
|
tlmFields :: InsOrdHashMap.InsOrdHashMap (Column b) (LogicalModelField b)
|
2023-04-19 12:03:36 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
instance (Backend b) => HasCodec (TrackLogicalModel b) where
|
|
|
|
codec =
|
|
|
|
AC.CommentCodec
|
|
|
|
("A request to track a logical model")
|
2023-04-25 11:59:34 +03:00
|
|
|
$ AC.object (backendPrefix @b <> "TrackLogicalModel")
|
2023-04-19 12:03:36 +03:00
|
|
|
$ TrackLogicalModel
|
2023-05-24 16:51:56 +03:00
|
|
|
<$> AC.requiredField "source" sourceDoc
|
|
|
|
AC..= tlmSource
|
2023-04-19 12:03:36 +03:00
|
|
|
<*> AC.requiredField "name" nameDoc
|
2023-05-24 16:51:56 +03:00
|
|
|
AC..= tlmName
|
2023-04-19 12:03:36 +03:00
|
|
|
<*> AC.optionalField "description" descriptionDoc
|
2023-05-24 16:51:56 +03:00
|
|
|
AC..= tlmDescription
|
2023-04-19 12:03:36 +03:00
|
|
|
<*> AC.requiredFieldWith "fields" logicalModelFieldMapCodec fieldsDoc
|
2023-05-24 16:51:56 +03:00
|
|
|
AC..= tlmFields
|
2023-04-19 12:03:36 +03:00
|
|
|
where
|
|
|
|
sourceDoc = "The source in which this logical model should be tracked"
|
|
|
|
nameDoc = "Root field name for the logical model"
|
|
|
|
fieldsDoc = "Return type of the expression"
|
|
|
|
descriptionDoc = "A description of the query which appears in the graphql schema"
|
|
|
|
|
|
|
|
deriving via
|
|
|
|
(AC.Autodocodec (TrackLogicalModel b))
|
|
|
|
instance
|
|
|
|
(Backend b) => FromJSON (TrackLogicalModel b)
|
|
|
|
|
|
|
|
deriving via
|
|
|
|
(AC.Autodocodec (TrackLogicalModel b))
|
|
|
|
instance
|
|
|
|
(Backend b) => ToJSON (TrackLogicalModel b)
|
|
|
|
|
|
|
|
-- | Validate a logical model and extract the logical model info from the request.
|
|
|
|
logicalModelTrackToMetadata ::
|
|
|
|
forall b.
|
|
|
|
TrackLogicalModel b ->
|
|
|
|
LogicalModelMetadata b
|
|
|
|
logicalModelTrackToMetadata TrackLogicalModel {..} =
|
|
|
|
LogicalModelMetadata
|
|
|
|
{ _lmmName = tlmName,
|
|
|
|
_lmmFields = tlmFields,
|
|
|
|
_lmmSelectPermissions = mempty,
|
|
|
|
_lmmDescription = tlmDescription
|
|
|
|
}
|
|
|
|
|
|
|
|
-- | API payload for the 'get_logical_model' endpoint.
|
|
|
|
data GetLogicalModel (b :: BackendType) = GetLogicalModel
|
|
|
|
{ glmSource :: SourceName
|
|
|
|
}
|
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
deriving instance (Backend b) => Show (GetLogicalModel b)
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
deriving instance (Backend b) => Eq (GetLogicalModel b)
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
instance (Backend b) => FromJSON (GetLogicalModel b) where
|
2023-04-19 12:03:36 +03:00
|
|
|
parseJSON = withObject "GetLogicalModel" $ \o -> do
|
|
|
|
glmSource <- o .: "source"
|
|
|
|
pure GetLogicalModel {..}
|
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
instance (Backend b) => ToJSON (GetLogicalModel b) where
|
2023-04-19 12:03:36 +03:00
|
|
|
toJSON GetLogicalModel {..} =
|
|
|
|
object
|
|
|
|
[ "source" .= glmSource
|
|
|
|
]
|
|
|
|
|
|
|
|
-- | Handler for the 'get_logical_model' endpoint.
|
|
|
|
runGetLogicalModel ::
|
|
|
|
forall b m.
|
|
|
|
( BackendMetadata b,
|
2023-06-05 18:15:17 +03:00
|
|
|
MonadError QErr m,
|
2023-05-04 15:07:32 +03:00
|
|
|
MetadataM m
|
2023-04-19 12:03:36 +03:00
|
|
|
) =>
|
|
|
|
GetLogicalModel b ->
|
|
|
|
m EncJSON
|
|
|
|
runGetLogicalModel q = do
|
2023-06-05 18:15:17 +03:00
|
|
|
maybe
|
|
|
|
( throw400 NotFound
|
|
|
|
$ "Source '"
|
|
|
|
<> sourceNameToText (glmSource q)
|
|
|
|
<> "' of kind "
|
|
|
|
<> toTxt (reify (backendTag @b))
|
|
|
|
<> " not found."
|
|
|
|
)
|
|
|
|
(const $ pure ())
|
|
|
|
. preview (metaSources . ix (glmSource q) . toSourceMetadata @b)
|
|
|
|
=<< getMetadata
|
2023-04-19 12:03:36 +03:00
|
|
|
metadata <- getMetadata
|
|
|
|
|
|
|
|
let logicalModels :: Maybe (LogicalModels b)
|
2023-06-13 16:29:29 +03:00
|
|
|
logicalModels = metadata ^? getLogicalModels (glmSource q)
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-04-27 10:41:55 +03:00
|
|
|
pure (encJFromJValue (InsOrdHashMap.elems <$> logicalModels))
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-06-13 16:29:29 +03:00
|
|
|
getLogicalModels :: forall b. (Backend b) => SourceName -> Traversal' Metadata (LogicalModels b)
|
|
|
|
getLogicalModels sourceName =
|
2023-04-19 12:03:36 +03:00
|
|
|
metaSources . ix sourceName . toSourceMetadata . smLogicalModels @b
|
|
|
|
|
|
|
|
-- | Handler for the 'track_logical_model' endpoint. The type 'TrackLogicalModel b'
|
|
|
|
-- (appearing here in wrapped as 'BackendTrackLogicalModel b' for 'AnyBackend'
|
|
|
|
-- compatibility) is defined in 'class LogicalModelMetadata'.
|
2023-06-14 23:43:24 +03:00
|
|
|
execTrackLogicalModel ::
|
2023-04-19 12:03:36 +03:00
|
|
|
forall b m.
|
|
|
|
( BackendMetadata b,
|
2023-05-04 15:07:32 +03:00
|
|
|
MonadError QErr m
|
2023-04-19 12:03:36 +03:00
|
|
|
) =>
|
|
|
|
TrackLogicalModel b ->
|
2023-06-14 23:43:24 +03:00
|
|
|
Metadata ->
|
|
|
|
m (MetadataObjId, MetadataModifier)
|
|
|
|
execTrackLogicalModel trackLogicalModelRequest metadata = do
|
2023-06-12 09:51:11 +03:00
|
|
|
-- validation
|
2023-04-19 12:03:36 +03:00
|
|
|
sourceMetadata <-
|
|
|
|
maybe (throw400 NotFound $ "Source " <> sourceNameToText source <> " not found.") pure
|
|
|
|
. preview (metaSources . ix source . toSourceMetadata @b)
|
2023-06-14 23:43:24 +03:00
|
|
|
$ metadata
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-06-14 23:43:24 +03:00
|
|
|
let (lmMetadata :: LogicalModelMetadata b) = logicalModelTrackToMetadata trackLogicalModelRequest
|
|
|
|
fieldName = _lmmName lmMetadata
|
2023-06-12 09:51:11 +03:00
|
|
|
existingLogicalModels = InsOrdHashMap.keys (_smLogicalModels sourceMetadata)
|
|
|
|
|
|
|
|
when (fieldName `elem` existingLogicalModels) do
|
|
|
|
throw400 AlreadyTracked $ "Logical model '" <> toTxt fieldName <> "' is already tracked."
|
|
|
|
|
2023-06-14 23:43:24 +03:00
|
|
|
let metadataObj =
|
2023-05-24 16:51:56 +03:00
|
|
|
MOSourceObjId source
|
|
|
|
$ AB.mkAnyBackend
|
|
|
|
$ SMOLogicalModel @b fieldName
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-06-12 09:51:11 +03:00
|
|
|
let metadataModifier =
|
|
|
|
MetadataModifier
|
|
|
|
$ (metaSources . ix source . toSourceMetadata @b . smLogicalModels)
|
2023-06-14 23:43:24 +03:00
|
|
|
%~ InsOrdHashMap.insert fieldName lmMetadata
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-06-12 09:51:11 +03:00
|
|
|
pure (metadataObj, metadataModifier)
|
2023-04-19 12:03:36 +03:00
|
|
|
where
|
|
|
|
source = tlmSource trackLogicalModelRequest
|
|
|
|
|
|
|
|
-- | API payload for the 'untrack_logical_model' endpoint.
|
|
|
|
data UntrackLogicalModel (b :: BackendType) = UntrackLogicalModel
|
|
|
|
{ utlmSource :: SourceName,
|
|
|
|
utlmName :: LogicalModelName
|
|
|
|
}
|
|
|
|
|
|
|
|
deriving instance Show (UntrackLogicalModel b)
|
|
|
|
|
|
|
|
deriving instance Eq (UntrackLogicalModel b)
|
|
|
|
|
|
|
|
instance FromJSON (UntrackLogicalModel b) where
|
|
|
|
parseJSON = withObject "UntrackLogicalModel" $ \o -> do
|
|
|
|
utlmSource <- o .: "source"
|
|
|
|
utlmName <- o .: "name"
|
|
|
|
pure UntrackLogicalModel {..}
|
|
|
|
|
|
|
|
instance ToJSON (UntrackLogicalModel b) where
|
|
|
|
toJSON UntrackLogicalModel {..} =
|
|
|
|
object
|
|
|
|
[ "source" .= utlmSource,
|
|
|
|
"name" .= utlmName
|
|
|
|
]
|
|
|
|
|
2023-06-12 09:51:11 +03:00
|
|
|
-- | Handler for the 'untrack_logical_model' endpoint.
|
|
|
|
execUntrackLogicalModel ::
|
|
|
|
forall b m.
|
|
|
|
( BackendMetadata b,
|
2023-06-14 23:43:24 +03:00
|
|
|
MonadError QErr m
|
2023-06-12 09:51:11 +03:00
|
|
|
) =>
|
|
|
|
UntrackLogicalModel b ->
|
2023-06-14 23:43:24 +03:00
|
|
|
Metadata ->
|
2023-06-12 09:51:11 +03:00
|
|
|
m (MetadataObjId, MetadataModifier)
|
2023-06-14 23:43:24 +03:00
|
|
|
execUntrackLogicalModel q metadata = do
|
2023-06-12 09:51:11 +03:00
|
|
|
-- we do not check for feature flag here as we always want users to be able
|
|
|
|
-- to remove logical models if they'd like
|
2023-06-14 23:43:24 +03:00
|
|
|
assertLogicalModelExists @b source fieldName metadata
|
2023-06-12 09:51:11 +03:00
|
|
|
|
|
|
|
let metadataObj =
|
|
|
|
MOSourceObjId source
|
|
|
|
$ AB.mkAnyBackend
|
|
|
|
$ SMOLogicalModel @b fieldName
|
|
|
|
|
|
|
|
pure (metadataObj, dropLogicalModelInMetadata @b source fieldName)
|
|
|
|
where
|
|
|
|
source = utlmSource q
|
|
|
|
fieldName = utlmName q
|
|
|
|
|
2023-04-19 12:03:36 +03:00
|
|
|
-- | A permission for logical models is tied to a specific name and
|
|
|
|
-- source. This wrapper adds both of those things to the JSON object that
|
|
|
|
-- describes the permission.
|
|
|
|
data CreateLogicalModelPermission a (b :: BackendType) = CreateLogicalModelPermission
|
|
|
|
{ clmpSource :: SourceName,
|
|
|
|
clmpName :: LogicalModelName,
|
|
|
|
clmpInfo :: PermDef b a
|
|
|
|
}
|
|
|
|
deriving stock (Generic)
|
|
|
|
|
|
|
|
instance
|
2023-05-24 16:51:56 +03:00
|
|
|
(FromJSON (PermDef b a)) =>
|
2023-04-19 12:03:36 +03:00
|
|
|
FromJSON (CreateLogicalModelPermission a b)
|
|
|
|
where
|
|
|
|
parseJSON = withObject "CreateLogicalModelPermission" \obj -> do
|
|
|
|
clmpSource <- obj .:? "source" .!= defaultSource
|
|
|
|
clmpName <- obj .: "name"
|
|
|
|
clmpInfo <- parseJSON (Object obj)
|
|
|
|
|
|
|
|
pure CreateLogicalModelPermission {..}
|
|
|
|
|
|
|
|
runCreateSelectLogicalModelPermission ::
|
|
|
|
forall b m.
|
2023-05-04 15:07:32 +03:00
|
|
|
(Backend b, CacheRWM m, MetadataM m, MonadError QErr m) =>
|
2023-04-19 12:03:36 +03:00
|
|
|
CreateLogicalModelPermission SelPerm b ->
|
|
|
|
m EncJSON
|
|
|
|
runCreateSelectLogicalModelPermission CreateLogicalModelPermission {..} = do
|
2023-06-14 23:43:24 +03:00
|
|
|
metadata <- getMetadata
|
|
|
|
assertLogicalModelExists @b clmpSource clmpName metadata
|
2023-04-19 12:03:36 +03:00
|
|
|
|
|
|
|
let metadataObj :: MetadataObjId
|
|
|
|
metadataObj =
|
2023-05-24 16:51:56 +03:00
|
|
|
MOSourceObjId clmpSource
|
|
|
|
$ AB.mkAnyBackend
|
|
|
|
$ SMOLogicalModel @b clmpName
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
buildSchemaCacheFor metadataObj
|
|
|
|
$ MetadataModifier
|
|
|
|
$ logicalModelMetadataSetter @b clmpSource clmpName
|
|
|
|
. lmmSelectPermissions
|
|
|
|
%~ InsOrdHashMap.insert (_pdRole clmpInfo) clmpInfo
|
2023-04-19 12:03:36 +03:00
|
|
|
|
|
|
|
pure successMsg
|
|
|
|
|
|
|
|
-- | To drop a permission, we need to know the source and name of
|
|
|
|
-- the logical model, as well as the role whose permission we want to drop.
|
|
|
|
data DropLogicalModelPermission (b :: BackendType) = DropLogicalModelPermission
|
|
|
|
{ dlmpSource :: SourceName,
|
|
|
|
dlmpName :: LogicalModelName,
|
|
|
|
dlmpRole :: RoleName
|
|
|
|
}
|
|
|
|
deriving stock (Generic)
|
|
|
|
|
|
|
|
instance FromJSON (DropLogicalModelPermission b) where
|
|
|
|
parseJSON = withObject "DropLogicalModelPermission" \obj -> do
|
|
|
|
dlmpSource <- obj .:? "source" .!= defaultSource
|
|
|
|
dlmpName <- obj .: "name"
|
|
|
|
dlmpRole <- obj .: "role"
|
|
|
|
|
|
|
|
pure DropLogicalModelPermission {..}
|
|
|
|
|
|
|
|
runDropSelectLogicalModelPermission ::
|
|
|
|
forall b m.
|
2023-05-04 15:07:32 +03:00
|
|
|
(Backend b, CacheRWM m, MetadataM m, MonadError QErr m) =>
|
2023-04-19 12:03:36 +03:00
|
|
|
DropLogicalModelPermission b ->
|
|
|
|
m EncJSON
|
|
|
|
runDropSelectLogicalModelPermission DropLogicalModelPermission {..} = do
|
2023-06-14 23:43:24 +03:00
|
|
|
metadata <- getMetadata
|
|
|
|
assertLogicalModelExists @b dlmpSource dlmpName metadata
|
2023-04-19 12:03:36 +03:00
|
|
|
|
|
|
|
let metadataObj :: MetadataObjId
|
|
|
|
metadataObj =
|
2023-05-24 16:51:56 +03:00
|
|
|
MOSourceObjId dlmpSource
|
|
|
|
$ AB.mkAnyBackend
|
|
|
|
$ SMOLogicalModel @b dlmpName
|
2023-04-19 12:03:36 +03:00
|
|
|
|
2023-05-24 16:51:56 +03:00
|
|
|
buildSchemaCacheFor metadataObj
|
|
|
|
$ MetadataModifier
|
|
|
|
$ logicalModelMetadataSetter @b dlmpSource dlmpName
|
|
|
|
. lmmSelectPermissions
|
|
|
|
%~ InsOrdHashMap.delete dlmpRole
|
2023-04-19 12:03:36 +03:00
|
|
|
|
|
|
|
pure successMsg
|
|
|
|
|
|
|
|
-- | TODO: should this cascade and also delete associated permissions?
|
2023-05-24 16:51:56 +03:00
|
|
|
dropLogicalModelInMetadata :: forall b. (BackendMetadata b) => SourceName -> LogicalModelName -> MetadataModifier
|
2023-04-19 12:03:36 +03:00
|
|
|
dropLogicalModelInMetadata source name = do
|
2023-05-24 16:51:56 +03:00
|
|
|
MetadataModifier
|
|
|
|
$ metaSources
|
|
|
|
. ix source
|
|
|
|
. toSourceMetadata @b
|
|
|
|
. smLogicalModels
|
|
|
|
%~ InsOrdHashMap.delete name
|
2023-04-19 12:03:36 +03:00
|
|
|
|
|
|
|
-- | Check whether a logical model with the given root field name exists for
|
|
|
|
-- the given source.
|
2023-06-14 23:43:24 +03:00
|
|
|
assertLogicalModelExists ::
|
|
|
|
forall b m. (Backend b, MonadError QErr m) => SourceName -> LogicalModelName -> Metadata -> m ()
|
|
|
|
assertLogicalModelExists sourceName name metadata = do
|
2023-04-19 12:03:36 +03:00
|
|
|
let sourceMetadataTraversal :: Traversal' Metadata (SourceMetadata b)
|
|
|
|
sourceMetadataTraversal = metaSources . ix sourceName . toSourceMetadata @b
|
|
|
|
|
|
|
|
sourceMetadata <-
|
|
|
|
preview sourceMetadataTraversal metadata
|
|
|
|
`onNothing` throw400 NotFound ("Source " <> sourceName <<> " not found.")
|
|
|
|
|
|
|
|
let desiredLogicalModel :: Traversal' (SourceMetadata b) (LogicalModelMetadata b)
|
|
|
|
desiredLogicalModel = smLogicalModels . ix name
|
|
|
|
|
|
|
|
unless (has desiredLogicalModel sourceMetadata) do
|
|
|
|
throw400 NotFound ("Logical model " <> name <<> " not found in source " <> sourceName <<> ".")
|