graphql-engine/server/src-lib/Hasura/GraphQL/Schema/Table.hs
Antoine Leblanc 0aaf006c25 server: add metadata for DB-DB remote joins
### Description

This PR is the first of several PRs meant to introduce Generalized Joins. In this first PR, we add non-breaking changes to the Metadata types for DB-to-DB remote joins. Note that we are currently rejecting the new remote join format in order to keep folks from breaking their metadata (in case of a downgrade). These issues will be tackled (and JSON changes reverted) in subsequent PRs.

This PR also changes the way we construct the schema cache, and breaks the way we process sources in two steps: we first resolve each source and construct a cache of their tables' raw info, then in a second step we build the source output. This is so that we have access to the target source's tables when building db-to-db relationships.

### Notes

- this PR contains a few minor cleanups of the schema
- it also fixes a bug in how we do renames in remote schema relationships
- it introduces cross-source schema dependencies

https://github.com/hasura/graphql-engine-mono/pull/1727

Co-authored-by: Evie Ciobanu <1017953+eviefp@users.noreply.github.com>
GitOrigin-RevId: f625473077bc5fff5d941b70e9a116192bc1eb22
2021-07-22 23:07:10 +00:00

186 lines
7.1 KiB
Haskell

-- | Helper functions for generating the schema of database tables
module Hasura.GraphQL.Schema.Table
( getTableGQLName
, tableSelectColumnsEnum
, tableUpdateColumnsEnum
, tablePermissions
, tableSelectPermissions
, tableSelectFields
, tableColumns
, tableSelectColumns
, tableUpdateColumns
) where
import Hasura.Prelude
import qualified Data.HashMap.Strict as Map
import qualified Data.HashSet as Set
import qualified Language.GraphQL.Draft.Syntax as G
import Data.Text.Extended
import qualified Hasura.GraphQL.Parser as P
import Hasura.Base.Error (QErr)
import Hasura.GraphQL.Parser (Kind (..), Parser)
import Hasura.GraphQL.Parser.Class
import Hasura.GraphQL.Schema.Backend
import Hasura.RQL.DML.Internal (getRolePermInfo)
import Hasura.RQL.Types
-- | Helper function to get the table GraphQL name. A table may have a
-- custom name configured with it. When the custom name exists, the GraphQL nodes
-- that are generated according to the custom name. For example: Let's say,
-- we have a table called `users address`, the name of the table is not GraphQL
-- compliant so we configure the table with a GraphQL compliant name,
-- say `users_address`
-- The generated top-level nodes of this table will be like `users_address`,
-- `insert_users_address` etc
getTableGQLName
:: forall b m. (Backend b, MonadError QErr m)
=> TableInfo b -> m G.Name
getTableGQLName tableInfo = do
let coreInfo = _tiCoreInfo tableInfo
tableName = _tciName coreInfo
tableCustomName = _tcCustomName $ _tciCustomConfig coreInfo
tableCustomName
`onNothing` tableGraphQLName @b tableName
`onLeft` throwError
-- | Table select columns enum
--
-- Parser for an enum type that matches the columns of the given
-- table. Used as a parameter for "distinct", among others. Maps to
-- the table_select_column object.
--
-- Return Nothing if there's no column the current user has "select"
-- permissions for.
tableSelectColumnsEnum
:: forall m n r b
. (BackendSchema b, MonadSchema n m, MonadRole r m, MonadTableInfo r m)
=> SourceName
-> TableInfo b
-> SelPermInfo b
-> m (Maybe (Parser 'Both n (Column b)))
tableSelectColumnsEnum sourceName tableInfo selectPermissions = do
tableGQLName <- getTableGQLName @b tableInfo
columns <- tableSelectColumns sourceName tableInfo selectPermissions
let enumName = tableGQLName <> $$(G.litName "_select_column")
description = Just $ G.Description $
"select columns of table " <>> tableInfoName tableInfo
pure $ P.enum enumName description <$> nonEmpty
[ ( define $ pgiName column
, pgiColumn column
)
| column <- columns
]
where
define name =
P.mkDefinition name (Just $ G.Description "column name") P.EnumValueInfo
-- | Table update columns enum
--
-- Parser for an enum type that matches the columns of the given
-- table. Used for conflict resolution in "insert" mutations, among
-- others. Maps to the table_update_column object.
--
-- If there's no column for which the current user has "update"
-- permissions, this functions returns an enum that only contains a
-- placeholder, so as to still allow this type to exist in the schema.
tableUpdateColumnsEnum
:: forall m n b
. (BackendSchema b, MonadSchema n m, MonadError QErr m)
=> TableInfo b
-> UpdPermInfo b
-> m (Parser 'Both n (Maybe (Column b)))
tableUpdateColumnsEnum tableInfo updatePermissions = do
tableGQLName <- getTableGQLName tableInfo
columns <- tableUpdateColumns tableInfo updatePermissions
let enumName = tableGQLName <> $$(G.litName "_update_column")
tableName = tableInfoName tableInfo
enumDesc = Just $ G.Description $ "update columns of table " <>> tableName
altDesc = Just $ G.Description $ "placeholder for update columns of table " <> tableName <<> " (current role has no relevant permissions)"
enumValues = do
column <- columns
pure (define $ pgiName column, Just $ pgiColumn column)
pure $ case nonEmpty enumValues of
Just values -> P.enum enumName enumDesc values
Nothing -> P.enum enumName altDesc $ pure (placeholder, Nothing)
where
define name = P.mkDefinition name (Just $ G.Description "column name") P.EnumValueInfo
placeholder = P.mkDefinition $$(G.litName "_PLACEHOLDER") (Just $ G.Description "placeholder (do not use)") P.EnumValueInfo
tablePermissions
:: forall m n r b. (Backend b, MonadSchema n m, MonadRole r m)
=> TableInfo b
-> m (Maybe (RolePermInfo b))
tablePermissions tableInfo = do
roleName <- askRoleName
pure $ getRolePermInfo roleName tableInfo
tableSelectPermissions
:: forall b r m n. (Backend b, MonadSchema n m, MonadRole r m)
=> TableInfo b
-> m (Maybe (SelPermInfo b))
tableSelectPermissions tableInfo = (_permSel =<<) <$> tablePermissions tableInfo
tableSelectFields
:: forall m n r b. (Backend b, MonadSchema n m, MonadTableInfo r m, MonadRole r m)
=> SourceName
-> TableInfo b
-> SelPermInfo b
-> m [FieldInfo b]
tableSelectFields sourceName tableInfo permissions = do
let tableFields = _tciFieldInfoMap . _tiCoreInfo $ tableInfo
filterM canBeSelected $ Map.elems tableFields
where
canBeSelected (FIColumn columnInfo) =
pure $ Map.member (pgiColumn columnInfo) (spiCols permissions)
canBeSelected (FIRelationship relationshipInfo) = do
tableInfo' <- askTableInfo sourceName $ riRTable relationshipInfo
isJust <$> tableSelectPermissions @b tableInfo'
canBeSelected (FIComputedField computedFieldInfo) =
case _cfiReturnType computedFieldInfo of
CFRScalar _ ->
pure $ Map.member (_cfiName computedFieldInfo) $ spiScalarComputedFields permissions
CFRSetofTable tableName -> do
tableInfo' <- askTableInfo sourceName tableName
isJust <$> tableSelectPermissions @b tableInfo'
canBeSelected (FIRemoteRelationship _) = pure True
tableColumns
:: forall b. TableInfo b -> [ColumnInfo b]
tableColumns tableInfo =
mapMaybe columnInfo . Map.elems . _tciFieldInfoMap . _tiCoreInfo $ tableInfo
where
columnInfo (FIColumn ci) = Just ci
columnInfo _ = Nothing
tableSelectColumns
:: forall m n r b. (Backend b, MonadSchema n m, MonadTableInfo r m, MonadRole r m)
=> SourceName
-> TableInfo b
-> SelPermInfo b
-> m [ColumnInfo b]
tableSelectColumns sourceName tableInfo permissions =
mapMaybe columnInfo <$> tableSelectFields sourceName tableInfo permissions
where
columnInfo (FIColumn ci) = Just ci
columnInfo _ = Nothing
tableUpdateColumns
:: forall m n b. (Backend b, MonadSchema n m)
=> TableInfo b
-> UpdPermInfo b
-> m [ColumnInfo b]
tableUpdateColumns tableInfo permissions = do
let tableFields = _tciFieldInfoMap . _tiCoreInfo $ tableInfo
pure $ mapMaybe isUpdatable $ Map.elems tableFields
where
isUpdatable (FIColumn columnInfo) =
if Set.member (pgiColumn columnInfo) (upiCols permissions)
&& not (Map.member (pgiColumn columnInfo) (upiSet permissions))
then Just columnInfo
else Nothing
isUpdatable _ = Nothing