mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-16 01:44:03 +03:00
346804fc67
## Description This change adds support for nested object fields in HGE IR and Schema Cache, the Data Connectors backend and API, and the MongoDB agent. ### Data Connector API changes - The `/schema` endpoint response now includes an optional set of GraphQL type definitions. Table column types can refer to these definitions by name. - Queries can now include a new field type `object` which contains a column name and a nested query. This allows querying into a nested object within a field. ### MongoDB agent changes - Add support for querying into nested documents using the new `object` field type. ### HGE changes - The `Backend` type class has a new type family `XNestedObjects b` which controls whether or not a backend supports querying into nested objects. This is currently enabled only for the `DataConnector` backend. - For backends that support nested objects, the `FieldInfo` type gets a new constructor `FINestedObject`, and the `AnnFieldG` type gets a new constructor `AFNestedObject`. - If the DC `/schema` endpoint returns any custom GraphQL type definitions they are stored in the `TableInfo` for each table in the source. - During schema cache building, the function `addNonColumnFields` will check whether any column types match custom GraphQL object types stored in the `TableInfo`. If so, they are converted into `FINestedObject` instead of `FIColumn` in the `FieldInfoMap`. - When building the `FieldParser`s from `FieldInfo` (function `fieldSelection`) any `FINestedObject` fields are converted into nested object parsers returning `AFNestedObject`. - The `DataConnector` query planner converts `AFNestedObject` fields into `object` field types in the query sent to the agent. ## Limitations ### HGE not yet implemented: - Support for nested arrays - Support for nested objects/arrays in mutations - Support for nested objects/arrays in order-by - Support for filters (`where`) in nested objects/arrays - Support for adding custom GraphQL types via track table metadata API - Support for interface and union types - Tests for nested objects ### Mongo agent not yet implemented: - Generate nested object types from validation schema - Support for aggregates - Support for order-by - Configure agent port - Build agent in CI - Agent tests for nested objects and MongoDB agent PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7844 GitOrigin-RevId: aec9ec1e4216293286a68f9b1af6f3f5317db423
259 lines
8.6 KiB
Haskell
259 lines
8.6 KiB
Haskell
-- | Internal helper module. Some things re-exported by
|
|
-- 'Test.Parser.Expectation'.
|
|
module Test.Parser.Internal
|
|
( mkTable,
|
|
ColumnInfoBuilder (..),
|
|
mkColumnInfo,
|
|
mkParser,
|
|
Parser,
|
|
TableInfoBuilder (..),
|
|
tableInfoBuilder,
|
|
buildTableInfo,
|
|
)
|
|
where
|
|
|
|
import Data.HashMap.Strict qualified as HM
|
|
import Data.HashSet qualified as HS
|
|
import Data.Sequence.NonEmpty qualified as NESeq
|
|
import Data.Text.Casing qualified as C
|
|
import Hasura.Backends.Postgres.Instances.Schema ()
|
|
import Hasura.Backends.Postgres.SQL.Types (ConstraintName (..), QualifiedObject (..), QualifiedTable, TableName (..), unsafePGCol)
|
|
import Hasura.GraphQL.Schema.Backend
|
|
import Hasura.GraphQL.Schema.Common (Scenario (Frontend))
|
|
import Hasura.GraphQL.Schema.Parser (FieldParser)
|
|
import Hasura.Prelude
|
|
import Hasura.RQL.IR.BoolExp (AnnBoolExpFld (..), GBoolExp (..), PartialSQLExp (..))
|
|
import Hasura.RQL.IR.Root (RemoteRelationshipField)
|
|
import Hasura.RQL.IR.Update (AnnotatedUpdateG (..))
|
|
import Hasura.RQL.IR.Value (UnpreparedValue (..))
|
|
import Hasura.RQL.Types.Column (ColumnInfo (..), ColumnMutability (..), ColumnType (..))
|
|
import Hasura.RQL.Types.Common (Comment (..), FieldName (..), OID (..))
|
|
import Hasura.RQL.Types.Instances ()
|
|
import Hasura.RQL.Types.Permission (AllowedRootFields (..))
|
|
import Hasura.RQL.Types.Relationships.Local (RelInfo (..), fromRel)
|
|
import Hasura.RQL.Types.Table (Constraint (..), CustomRootField (..), FieldInfo (..), PrimaryKey (..), RolePermInfo (..), SelPermInfo (..), TableConfig (..), TableCoreInfoG (..), TableCustomRootFields (..), TableInfo (..), UpdPermInfo (..))
|
|
import Hasura.SQL.Backend (BackendType (Postgres), PostgresKind (Vanilla))
|
|
import Language.GraphQL.Draft.Syntax (unsafeMkName)
|
|
import Test.Parser.Monad
|
|
|
|
{-# ANN module ("HLint: ignore Use mkName" :: String) #-}
|
|
|
|
type PG = 'Postgres 'Vanilla
|
|
|
|
type Parser = FieldParser ParserTest (AnnotatedUpdateG PG (RemoteRelationshipField UnpreparedValue) (UnpreparedValue PG))
|
|
|
|
-- | Create a table by its name, using the public schema.
|
|
mkTable :: Text -> QualifiedTable
|
|
mkTable name =
|
|
QualifiedObject
|
|
{ qSchema = "public",
|
|
qName = TableName name
|
|
}
|
|
|
|
-- | Build a column, see 'mkColumnInfo'.
|
|
data ColumnInfoBuilder = ColumnInfoBuilder
|
|
{ -- | name of the column
|
|
cibName :: Text,
|
|
-- | Column type, e.g.
|
|
--
|
|
-- > ColumnScalar PGText
|
|
cibType :: ColumnType PG,
|
|
-- | whether the column is nullable or not
|
|
cibNullable :: Bool,
|
|
-- | is it a primary key?
|
|
cibIsPrimaryKey :: Bool
|
|
}
|
|
|
|
-- | Create a column using the provided 'ColumnInfoBuilder' and defaults.
|
|
--
|
|
-- Note that all permissions are enabled by default.
|
|
mkColumnInfo :: ColumnInfoBuilder -> ColumnInfo PG
|
|
mkColumnInfo ColumnInfoBuilder {..} =
|
|
ColumnInfo
|
|
{ ciColumn = unsafePGCol cibName,
|
|
ciName = unsafeMkName cibName,
|
|
ciPosition = 0,
|
|
ciType = cibType,
|
|
ciIsNullable = cibNullable,
|
|
ciDescription = Nothing,
|
|
ciMutability = columnMutability
|
|
}
|
|
where
|
|
columnMutability :: ColumnMutability
|
|
columnMutability =
|
|
ColumnMutability
|
|
{ _cmIsInsertable = True,
|
|
_cmIsUpdatable = True
|
|
}
|
|
|
|
-- | Create a parser for the provided table and columns.
|
|
--
|
|
-- No special permissions, required headers, filters, etc., are set.
|
|
--
|
|
-- This will not work for inserts and deletes (see @rolePermInfo@ below).
|
|
mkParser :: TableInfoBuilder -> SchemaTest [Parser]
|
|
mkParser tib =
|
|
buildTableUpdateMutationFields
|
|
Frontend
|
|
(buildTableInfo tib)
|
|
name
|
|
where
|
|
name :: C.GQLNameIdentifier
|
|
name = C.fromAutogeneratedName (unsafeMkName $ getTableTxt $ qName (table tib))
|
|
|
|
-- | Inputs for building 'TableInfo's.
|
|
-- The expectation is that this will be extended freely as new tests need more
|
|
-- elaborate setup.
|
|
data TableInfoBuilder = TableInfoBuilder
|
|
{ table :: QualifiedTable,
|
|
columns :: [ColumnInfoBuilder],
|
|
relations :: [RelInfo PG]
|
|
}
|
|
|
|
-- | A smart constructor for an empty 'TableInfoBuilder'.
|
|
-- This should make it easier to maintain existing test code when new fields are
|
|
-- added.
|
|
tableInfoBuilder :: QualifiedTable -> TableInfoBuilder
|
|
tableInfoBuilder table = TableInfoBuilder {columns = [], relations = [], ..}
|
|
|
|
-- | Build a 'TableInfo' from a 'TableInfoBuilder.
|
|
-- The expectation is that this will be extended freely as new tests need more
|
|
-- elaborate setup.
|
|
buildTableInfo :: TableInfoBuilder -> TableInfo PG
|
|
buildTableInfo TableInfoBuilder {..} = tableInfo
|
|
where
|
|
tableInfo :: TableInfo PG
|
|
tableInfo =
|
|
TableInfo
|
|
{ _tiCoreInfo = tableCoreInfo,
|
|
_tiRolePermInfoMap = mempty,
|
|
_tiEventTriggerInfoMap = mempty,
|
|
_tiAdminRolePermInfo = rolePermInfo
|
|
}
|
|
|
|
tableCoreInfo :: TableCoreInfoG PG (FieldInfo PG) (ColumnInfo PG)
|
|
tableCoreInfo =
|
|
TableCoreInfo
|
|
{ _tciName = table,
|
|
_tciDescription = Nothing,
|
|
_tciFieldInfoMap = fieldInfoMap,
|
|
_tciPrimaryKey = pk,
|
|
_tciUniqueConstraints = mempty,
|
|
_tciForeignKeys = mempty,
|
|
_tciViewInfo = Nothing,
|
|
_tciEnumValues = Nothing,
|
|
_tciCustomConfig = tableConfig,
|
|
_tciExtraTableMetadata = (),
|
|
_tciApolloFederationConfig = Nothing,
|
|
_tciCustomObjectTypes = mempty
|
|
}
|
|
|
|
pk :: Maybe (PrimaryKey PG (ColumnInfo PG))
|
|
pk = case pks of
|
|
Nothing -> Nothing
|
|
Just primaryColumns ->
|
|
Just
|
|
PrimaryKey
|
|
{ _pkConstraint =
|
|
Constraint
|
|
{ _cName = ConstraintName "",
|
|
_cOid = OID 0
|
|
},
|
|
_pkColumns = primaryColumns
|
|
}
|
|
|
|
rolePermInfo :: RolePermInfo PG
|
|
rolePermInfo =
|
|
RolePermInfo
|
|
{ _permIns = Nothing,
|
|
_permSel = Just selPermInfo,
|
|
_permUpd = Just updPermInfo,
|
|
_permDel = Nothing
|
|
}
|
|
|
|
fieldInfoMap :: HM.HashMap FieldName (FieldInfo PG)
|
|
fieldInfoMap = HM.unions [columnFields, relationFields]
|
|
|
|
columnFields :: HM.HashMap FieldName (FieldInfo PG)
|
|
columnFields =
|
|
HM.fromList
|
|
. fmap toCIHashPair
|
|
$ columns
|
|
|
|
toCIHashPair :: ColumnInfoBuilder -> (FieldName, FieldInfo PG)
|
|
toCIHashPair cib = (coerce $ cibName cib, FIColumn $ mkColumnInfo cib)
|
|
|
|
toRelHashPair :: RelInfo PG -> (FieldName, FieldInfo PG)
|
|
toRelHashPair ri = (fromRel $ riName ri, FIRelationship ri)
|
|
|
|
relationFields :: HM.HashMap FieldName (FieldInfo PG)
|
|
relationFields = HM.fromList . fmap toRelHashPair $ relations
|
|
|
|
tableConfig :: TableConfig PG
|
|
tableConfig =
|
|
TableConfig
|
|
{ _tcCustomRootFields = tableCustomRootFields,
|
|
_tcColumnConfig = mempty,
|
|
_tcCustomName = Nothing,
|
|
_tcComment = Automatic
|
|
}
|
|
|
|
selPermInfo :: SelPermInfo PG
|
|
selPermInfo =
|
|
SelPermInfo
|
|
{ spiCols = HM.fromList . fmap ((,Nothing) . unsafePGCol . cibName) $ columns,
|
|
spiComputedFields = mempty,
|
|
spiFilter = upiFilter,
|
|
spiLimit = Nothing,
|
|
spiAllowAgg = True,
|
|
spiRequiredHeaders = mempty,
|
|
spiAllowedQueryRootFields = ARFAllowAllRootFields,
|
|
spiAllowedSubscriptionRootFields = ARFAllowAllRootFields
|
|
}
|
|
|
|
tableCustomRootFields :: TableCustomRootFields
|
|
tableCustomRootFields =
|
|
TableCustomRootFields
|
|
{ _tcrfSelect = customRootField,
|
|
_tcrfSelectByPk = customRootField,
|
|
_tcrfSelectAggregate = customRootField,
|
|
_tcrfSelectStream = customRootField,
|
|
_tcrfInsert = customRootField,
|
|
_tcrfInsertOne = customRootField,
|
|
_tcrfUpdate = customRootField,
|
|
_tcrfUpdateByPk = customRootField,
|
|
_tcrfUpdateMany = customRootField,
|
|
_tcrfDelete = customRootField,
|
|
_tcrfDeleteByPk = customRootField
|
|
}
|
|
|
|
customRootField :: CustomRootField
|
|
customRootField =
|
|
CustomRootField
|
|
{ _crfName = Nothing,
|
|
_crfComment = Automatic
|
|
}
|
|
|
|
updPermInfo :: UpdPermInfo PG
|
|
updPermInfo =
|
|
UpdPermInfo
|
|
{ upiCols = HS.fromList . fmap (unsafePGCol . cibName) $ columns,
|
|
upiTable = table,
|
|
upiFilter = upiFilter,
|
|
upiCheck = Nothing,
|
|
upiSet = mempty,
|
|
upiBackendOnly = False,
|
|
upiRequiredHeaders = mempty
|
|
}
|
|
|
|
columnInfos :: [ColumnInfo PG]
|
|
columnInfos = mkColumnInfo <$> columns
|
|
|
|
pks :: Maybe (NESeq.NESeq (ColumnInfo PG))
|
|
pks = case mkColumnInfo <$> filter cibIsPrimaryKey columns of
|
|
[] -> Nothing
|
|
(x : xs) -> Just $ foldl (<>) (NESeq.singleton x) $ fmap NESeq.singleton xs
|
|
|
|
upiFilter :: GBoolExp PG (AnnBoolExpFld PG (PartialSQLExp PG))
|
|
upiFilter = BoolAnd $ fmap (\ci -> BoolField $ AVColumn ci []) columnInfos
|