graphql-engine/server/src-lib/Hasura/Table/Metadata.hs
David Overton 5b2970b44f Specify table schema via logical model in track_table API
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9408
GitOrigin-RevId: 9ba20cd91e4d0066e0f14ce52cba0f55a7c5af31
2023-06-22 07:06:40 +00:00

293 lines
9.1 KiB
Haskell

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE UndecidableInstances #-}
-- | In order to avoid circular dependencies while splitting
-- 'Hasura.RQL.Types.Metadata' into multiple modules, some definitions must be
-- moved out of that module. This module is the bucket for definitions that have
-- not been specifically moved elsewhere.
module Hasura.Table.Metadata
( ComputedFieldMetadata (..),
ComputedFields,
Permissions,
Relationships,
TableMetadata (..),
mkTableMeta,
tmArrayRelationships,
tmComputedFields,
tmConfiguration,
tmDeletePermissions,
tmApolloFederationConfig,
tmEventTriggers,
tmInsertPermissions,
tmIsEnum,
tmLogicalModel,
tmObjectRelationships,
tmRemoteRelationships,
tmSelectPermissions,
tmTable,
tmUpdatePermissions,
)
where
import Autodocodec hiding (object, (.=))
import Autodocodec qualified as AC
import Control.Lens hiding (set, (.=))
import Data.Aeson.KeyMap qualified as KM
import Data.Aeson.Types
import Data.HashMap.Strict.InsOrd.Autodocodec (sortedElemsCodec)
import Data.HashSet qualified as HS
import Data.List.Extended qualified as L
import Data.Text qualified as T
import Data.Text.Extended qualified as T
import Hasura.LogicalModel.Types
import Hasura.Prelude
import Hasura.RQL.Types.Backend
import Hasura.RQL.Types.BackendTag (backendPrefix)
import Hasura.RQL.Types.Common
import Hasura.RQL.Types.ComputedField
import Hasura.RQL.Types.EventTrigger
import Hasura.RQL.Types.Permission
import Hasura.RQL.Types.Relationships.Local
import Hasura.RQL.Types.Relationships.Remote
import Hasura.RQL.Types.Roles
import Hasura.Table.Cache (TableConfig (..), emptyTableConfig)
-- | Parse a list of objects into a map from a derived key,
-- failing if the list has duplicates.
parseListAsMap ::
(Hashable k, T.ToTxt k) =>
Text ->
(a -> k) ->
Parser [a] ->
Parser (InsOrdHashMap k a)
parseListAsMap things mapFn listP = do
list <- listP
let duplicates = toList $ L.duplicates $ map mapFn list
unless (null duplicates)
$ fail
$ T.unpack
$ "multiple declarations exist for the following "
<> things
<> ": "
<> T.commaSeparated duplicates
pure $ oMapFromL mapFn list
data ComputedFieldMetadata b = ComputedFieldMetadata
{ _cfmName :: ComputedFieldName,
_cfmDefinition :: ComputedFieldDefinition b,
_cfmComment :: Comment
}
deriving (Generic)
deriving instance (Backend b) => Show (ComputedFieldMetadata b)
deriving instance (Backend b) => Eq (ComputedFieldMetadata b)
instance (Backend b) => HasCodec (ComputedFieldMetadata b) where
codec =
AC.object (backendPrefix @b <> "ComputedFieldMetadata")
$ ComputedFieldMetadata
<$> requiredField' "name"
AC..= _cfmName
<*> requiredField' "definition"
AC..= _cfmDefinition
<*> optionalFieldWithOmittedDefault' "comment" Automatic
AC..= _cfmComment
instance (Backend b) => ToJSON (ComputedFieldMetadata b) where
toJSON ComputedFieldMetadata {..} =
object
$ [ "name" .= _cfmName,
"definition" .= _cfmDefinition,
"comment" .= _cfmComment
]
instance (Backend b) => FromJSON (ComputedFieldMetadata b) where
parseJSON = withObject "ComputedFieldMetadata" $ \obj ->
ComputedFieldMetadata
<$> obj
.: "name"
<*> obj
.: "definition"
<*> obj
.:? "comment"
.!= Automatic
type Relationships a = InsOrdHashMap RelName a
type ComputedFields b = InsOrdHashMap ComputedFieldName (ComputedFieldMetadata b)
type RemoteRelationships = InsOrdHashMap RelName RemoteRelationship
type Permissions a = InsOrdHashMap RoleName a
type EventTriggers b = InsOrdHashMap TriggerName (EventTriggerConf b)
data TableMetadata b = TableMetadata
{ _tmTable :: TableName b,
_tmIsEnum :: Bool,
_tmConfiguration :: TableConfig b,
_tmObjectRelationships :: Relationships (ObjRelDef b),
_tmArrayRelationships :: Relationships (ArrRelDef b),
_tmComputedFields :: ComputedFields b,
_tmRemoteRelationships :: RemoteRelationships,
_tmInsertPermissions :: Permissions (InsPermDef b),
_tmSelectPermissions :: Permissions (SelPermDef b),
_tmUpdatePermissions :: Permissions (UpdPermDef b),
_tmDeletePermissions :: Permissions (DelPermDef b),
_tmEventTriggers :: EventTriggers b,
_tmApolloFederationConfig :: Maybe ApolloFederationConfig,
_tmLogicalModel :: Maybe LogicalModelName
}
deriving (Generic)
deriving instance (Backend b) => Show (TableMetadata b)
deriving instance (Backend b) => Eq (TableMetadata b)
instance (Backend b) => ToJSON (TableMetadata b) where
toJSON = genericToJSON hasuraJSON
instance (Backend b) => HasCodec (TableMetadata b) where
codec =
CommentCodec "Representation of a table in metadata, 'tables.yaml' and 'metadata.json'"
$ AC.object (backendPrefix @b <> "TableMetadata")
$ TableMetadata
<$> requiredField' "table"
.== _tmTable
<*> optionalFieldWithOmittedDefault' "is_enum" False
.== _tmIsEnum
<*> optionalFieldWithOmittedDefault "configuration" emptyTableConfig configDoc
.== _tmConfiguration
<*> optSortedList "object_relationships" _rdName
.== _tmObjectRelationships
<*> optSortedList "array_relationships" _rdName
.== _tmArrayRelationships
<*> optSortedList "computed_fields" _cfmName
.== _tmComputedFields
<*> optSortedList "remote_relationships" _rrName
.== _tmRemoteRelationships
<*> optSortedList "insert_permissions" _pdRole
.== _tmInsertPermissions
<*> optSortedList "select_permissions" _pdRole
.== _tmSelectPermissions
<*> optSortedList "update_permissions" _pdRole
.== _tmUpdatePermissions
<*> optSortedList "delete_permissions" _pdRole
.== _tmDeletePermissions
<*> eventTriggers
<*> optionalFieldOrNull' "apollo_federation_config"
.== _tmApolloFederationConfig
<*> optionalFieldOrNull' "logical_model"
.== _tmLogicalModel
where
-- Some backends do not implement event triggers. In those cases we tailor
-- the codec to omit the @"event_triggers"@ field from the API.
eventTriggers = case defaultTriggerOnReplication @b of
Just _ -> optSortedList "event_triggers" etcName .== _tmEventTriggers
Nothing -> pure mempty
optSortedList ::
(HasCodec a, Eq a, Hashable k, Ord k, T.ToTxt k) =>
Text ->
(a -> k) ->
ObjectCodec (InsOrdHashMap k a) (InsOrdHashMap k a)
optSortedList name keyForElem =
AC.optionalFieldWithOmittedDefaultWith' name (sortedElemsCodec keyForElem) mempty
configDoc =
T.unlines
[ "Configuration for the table/view",
"",
"https://hasura.io/docs/latest/graphql/core/api-reference/schema-metadata-api/table-view.html#table-config"
]
(.==) = (AC..=)
$(makeLenses ''TableMetadata)
mkTableMeta :: TableName b -> Bool -> TableConfig b -> TableMetadata b
mkTableMeta qt isEnum config =
TableMetadata
qt
isEnum
config
mempty
mempty
mempty
mempty
mempty
mempty
mempty
mempty
mempty
Nothing
Nothing
instance (Backend b) => FromJSON (TableMetadata b) where
parseJSON = withObject "Object" $ \o -> do
let unexpectedKeys = getUnexpectedKeys o
unless (null unexpectedKeys)
$ fail
$ "unexpected keys when parsing TableMetadata: "
<> show (HS.toList unexpectedKeys)
TableMetadata
<$> o
.: tableKey
<*> o
.:? isEnumKey
.!= False
<*> o
.:? configKey
.!= emptyTableConfig
<*> parseListAsMap "object relationships" _rdName (o .:? orKey .!= [])
<*> parseListAsMap "array relationships" _rdName (o .:? arKey .!= [])
<*> parseListAsMap "computed fields" _cfmName (o .:? cfKey .!= [])
<*> parseListAsMap "remote relationships" _rrName (o .:? rrKey .!= [])
<*> parseListAsMap "insert permissions" _pdRole (o .:? ipKey .!= [])
<*> parseListAsMap "select permissions" _pdRole (o .:? spKey .!= [])
<*> parseListAsMap "update permissions" _pdRole (o .:? upKey .!= [])
<*> parseListAsMap "delete permissions" _pdRole (o .:? dpKey .!= [])
<*> parseListAsMap "event triggers" etcName (o .:? etKey .!= [])
<*> o
.:? enableAFKey
<*> o
.:? logicalModelKey
where
tableKey = "table"
isEnumKey = "is_enum"
configKey = "configuration"
orKey = "object_relationships"
arKey = "array_relationships"
ipKey = "insert_permissions"
spKey = "select_permissions"
upKey = "update_permissions"
dpKey = "delete_permissions"
etKey = "event_triggers"
cfKey = "computed_fields"
rrKey = "remote_relationships"
enableAFKey = "apollo_federation_config"
logicalModelKey = "logical_model"
getUnexpectedKeys o =
HS.fromList (KM.keys o) `HS.difference` expectedKeySet
expectedKeySet =
HS.fromList
[ tableKey,
isEnumKey,
configKey,
orKey,
arKey,
ipKey,
spKey,
upKey,
dpKey,
etKey,
cfKey,
rrKey,
enableAFKey,
logicalModelKey
]