mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-18 04:51:35 +03:00
e5f88d8039
## Description This change adds support for querying into nested arrays in Data Connector agents that support such a concept (currently MongoDB). ### DC API changes - New API type `ColumnType` which allows representing the type of a "column" as either a scalar type, an object reference or an array of `ColumnType`s. This recursive definition allows arbitrary nesting of arrays of types. - The `type` fields in the API types `ColumnInfo` and `ColumnInsertSchema` now take a `ColumnType` instead of a `ScalarType`. - To ensure backwards compatibility, a `ColumnType` representing a scalar serialises and deserialises to the same representation as `ScalarType`. - In queries, the `Field` type now has a new constructor `NestedArrayField`. This contains a nested `Field` along with optional `limit`, `offset`, `where` and `order_by` arguments. (These optional arguments are not yet used by either HGE or the MongoDB agent.) ### MongoDB Haskell agent changes - The `/schema` endpoint will now recognise arrays within the JSON validation schema and generate corresponding arrays in the DC schema. - The `/query` endpoint will now handle `NestedArrayField`s within queries (although it does not yet handle `limit`, `offset`, `where` and `order_by`). ### HGE server changes - The `Backend` type class adds a new type family `XNestedArrays b` to enable nested arrays on a per-backend basis (currently enabled only for the `DataConnector` backend. - Within `RawColumnInfo` the column type is now represented by a new type `RawColumnType b` which mirrors the shape of the DC API `ColumnType`, but uses `XNestedObjects b` and `XNestedArrays b` type families to allow turning nested object and array supports on or off for a particular backend. In the `DataConnector` backend `API.CustomType` is converted into `RawColumnInfo 'DataConnector` while building the schema. - In the next stage of schema building, the `RawColumnInfo` is converted into a `StructuredColumnInfo` which allows us to represent the three different types of columns: scalar, object and array. TODO: the `StructuredColumnInfo` looks very similar to the Logical Model types. The main difference is that it uses the `XNestedObjects` and `XNestedArrays` type families. We should be able to combine these two representations. - The `StructuredColumnInfo` is then placed into a `FIColumn` `FieldInfo`. This involved some refactoring of `FieldInfo` as I had previously split out `FINestedObject` into a separate constructor. However it works out better to represent all "column" fields (i.e. scalar, object and array) using `FIColumn` as this make it easier to implement permission checking correctly. This is the reason the `StructuredColumnInfo` was needed. - Next, the `FieldInfo` are used to generate `FieldParser`s. We add a new constructor to `AnnFieldG` for `AFNestedArray`. An `AFNestedArray` field parser can contain either a simple array selection or an array aggregate. Simple array `FieldParsers` are currently limited to subfield selection. We will add support for limit, offset, where and order_by in a future PR. We also don't yet generate array aggregate `FieldParsers. - The new `AFNestedArray` field is handled by the `QueryPlan` module in the `DataConnector` backend. There we generate an `API.NestedArrayField` from the AFNestedArray. We also handle nested arrays when reshaping the response from the DC agent. ## Limitations - Support for limit, offset, filter (where) and order_by is not yet fully implemented, although it should not be hard to add this - Support for aggregations on nested arrays is not yet fully implemented - Permissions involving nested arrays (and objects) not yet implemented - This should be integrated with Logical Model types, but that will happen in a separate PR PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9149 GitOrigin-RevId: 0e7b71a994fc1d2ca1ef73bfe7b96e95b5328531
168 lines
7.2 KiB
Haskell
168 lines
7.2 KiB
Haskell
-- | Postgres DDL Table
|
||
--
|
||
-- Used to fill up the enum values field of 'Hasura.RQL.Types.Table.TableCoreInfoG'.
|
||
--
|
||
-- See 'Hasura.Eventing.Backend'.
|
||
module Hasura.Backends.Postgres.DDL.Table
|
||
( fetchAndValidateEnumValues,
|
||
)
|
||
where
|
||
|
||
import Control.Monad.Trans.Control (MonadBaseControl)
|
||
import Control.Monad.Validate
|
||
import Data.HashMap.Strict qualified as HashMap
|
||
import Data.List (delete)
|
||
import Data.List.NonEmpty qualified as NE
|
||
import Data.Sequence qualified as Seq
|
||
import Data.Sequence.NonEmpty qualified as NESeq
|
||
import Data.Text.Extended
|
||
import Database.PG.Query qualified as PG
|
||
import Hasura.Backends.Postgres.Connection
|
||
import Hasura.Backends.Postgres.SQL.DML
|
||
import Hasura.Backends.Postgres.SQL.Types
|
||
import Hasura.Base.Error
|
||
import Hasura.Prelude
|
||
import Hasura.RQL.Types.Backend
|
||
import Hasura.RQL.Types.BackendType
|
||
import Hasura.RQL.Types.Column
|
||
import Hasura.SQL.Types
|
||
import Hasura.Server.Utils
|
||
import Hasura.Table.Cache
|
||
import Language.GraphQL.Draft.Syntax qualified as G
|
||
|
||
data EnumTableIntegrityError (b :: BackendType)
|
||
= EnumTablePostgresError Text
|
||
| EnumTableMissingPrimaryKey
|
||
| EnumTableMultiColumnPrimaryKey [PGCol]
|
||
| EnumTableNonTextualPrimaryKey (RawColumnInfo b)
|
||
| EnumTableNoEnumValues
|
||
| EnumTableInvalidEnumValueNames (NE.NonEmpty Text)
|
||
| EnumTableNonTextualCommentColumn (RawColumnInfo b)
|
||
| EnumTableTooManyColumns [PGCol]
|
||
|
||
fetchAndValidateEnumValues ::
|
||
forall pgKind m.
|
||
(Backend ('Postgres pgKind), MonadIO m, MonadBaseControl IO m) =>
|
||
PGSourceConfig ->
|
||
QualifiedTable ->
|
||
Maybe (PrimaryKey ('Postgres pgKind) (RawColumnInfo ('Postgres pgKind))) ->
|
||
[RawColumnInfo ('Postgres pgKind)] ->
|
||
m (Either QErr EnumValues)
|
||
fetchAndValidateEnumValues pgSourceConfig tableName maybePrimaryKey columnInfos =
|
||
runExceptT $
|
||
either (throw400 ConstraintViolation . showErrors) pure =<< runValidateT fetchAndValidate
|
||
where
|
||
fetchAndValidate ::
|
||
(MonadIO n, MonadBaseControl IO n, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] n) =>
|
||
n EnumValues
|
||
fetchAndValidate = do
|
||
maybePrimaryKeyColumn <- tolerate validatePrimaryKey
|
||
maybeCommentColumn <- validateColumns maybePrimaryKeyColumn
|
||
case maybePrimaryKeyColumn of
|
||
Nothing -> refute mempty
|
||
Just primaryKeyColumn -> do
|
||
result <-
|
||
runPgSourceReadTx pgSourceConfig $
|
||
runValidateT $
|
||
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn
|
||
case result of
|
||
Left e -> (refute . pure . EnumTablePostgresError . qeError) e
|
||
Right (Left vErrs) -> refute vErrs
|
||
Right (Right r) -> pure r
|
||
where
|
||
validatePrimaryKey = case maybePrimaryKey of
|
||
Nothing -> refute [EnumTableMissingPrimaryKey]
|
||
Just primaryKey -> case _pkColumns primaryKey of
|
||
column NESeq.:<|| Seq.Empty -> case rciType column of
|
||
RawColumnTypeScalar PGText -> pure column
|
||
_ -> refute [EnumTableNonTextualPrimaryKey column]
|
||
columns -> refute [EnumTableMultiColumnPrimaryKey $ map rciName (toList columns)]
|
||
|
||
validateColumns primaryKeyColumn = do
|
||
let nonPrimaryKeyColumns = maybe columnInfos (`delete` columnInfos) primaryKeyColumn
|
||
case nonPrimaryKeyColumns of
|
||
[] -> pure Nothing
|
||
[column] -> case rciType column of
|
||
RawColumnTypeScalar PGText -> pure $ Just column
|
||
_ -> dispute [EnumTableNonTextualCommentColumn column] $> Nothing
|
||
columns -> dispute [EnumTableTooManyColumns $ map rciName columns] $> Nothing
|
||
|
||
showErrors :: [EnumTableIntegrityError ('Postgres pgKind)] -> Text
|
||
showErrors allErrors =
|
||
"the table " <> tableName <<> " cannot be used as an enum " <> reasonsMessage
|
||
where
|
||
reasonsMessage = makeReasonMessage allErrors showOne
|
||
|
||
showOne :: EnumTableIntegrityError ('Postgres pgKind) -> Text
|
||
showOne = \case
|
||
EnumTablePostgresError err -> "postgres error: " <> err
|
||
EnumTableMissingPrimaryKey -> "the table must have a primary key"
|
||
EnumTableMultiColumnPrimaryKey cols ->
|
||
"the table’s primary key must not span multiple columns ("
|
||
<> commaSeparated (sort cols)
|
||
<> ")"
|
||
EnumTableNonTextualPrimaryKey colInfo -> typeMismatch "primary key" colInfo PGText
|
||
EnumTableNoEnumValues -> "the table must have at least one row"
|
||
EnumTableInvalidEnumValueNames values ->
|
||
let pluralString = " are not valid GraphQL enum value names"
|
||
valuesString = case NE.reverse (NE.sort values) of
|
||
value NE.:| [] -> "value " <> value <<> " is not a valid GraphQL enum value name"
|
||
value2 NE.:| [value1] -> "values " <> value1 <<> " and " <> value2 <<> pluralString
|
||
lastValue NE.:| otherValues ->
|
||
"values "
|
||
<> commaSeparated (reverse otherValues)
|
||
<> ", and "
|
||
<> lastValue <<> pluralString
|
||
in "the " <> valuesString
|
||
EnumTableNonTextualCommentColumn colInfo -> typeMismatch "comment column" colInfo PGText
|
||
EnumTableTooManyColumns cols ->
|
||
"the table must have exactly one primary key and optionally one comment column, not "
|
||
<> tshow (length cols)
|
||
<> " columns ("
|
||
<> commaSeparated (sort cols)
|
||
<> ")"
|
||
where
|
||
typeMismatch description colInfo expected =
|
||
let RawColumnTypeScalar scalarType = rciType @('Postgres pgKind) colInfo
|
||
in "the table’s "
|
||
<> description
|
||
<> " ("
|
||
<> rciName colInfo <<> ") must have type "
|
||
<> expected <<> ", not type " <>> scalarType
|
||
|
||
fetchEnumValuesFromDb ::
|
||
forall pgKind m.
|
||
(MonadTx m, MonadValidate [EnumTableIntegrityError ('Postgres pgKind)] m) =>
|
||
QualifiedTable ->
|
||
RawColumnInfo ('Postgres pgKind) ->
|
||
Maybe (RawColumnInfo ('Postgres pgKind)) ->
|
||
m EnumValues
|
||
fetchEnumValuesFromDb tableName primaryKeyColumn maybeCommentColumn = do
|
||
let nullExtr = Extractor SENull Nothing
|
||
commentExtr = maybe nullExtr (mkExtr . rciName) maybeCommentColumn
|
||
query =
|
||
PG.fromBuilder $
|
||
toSQL
|
||
mkSelect
|
||
{ selFrom = Just $ mkSimpleFromExp tableName,
|
||
selExtr = [mkExtr (rciName primaryKeyColumn), commentExtr]
|
||
}
|
||
rawEnumValues <- liftTx $ PG.withQE defaultTxErrorHandler query () True
|
||
when (null rawEnumValues) $ dispute [EnumTableNoEnumValues]
|
||
let enumValues = flip map rawEnumValues $
|
||
\(enumValueText, comment) ->
|
||
case mkValidEnumValueName enumValueText of
|
||
Nothing -> Left enumValueText
|
||
Just enumValue -> Right (EnumValue enumValue, EnumValueInfo comment)
|
||
badNames = lefts enumValues
|
||
validEnums = rights enumValues
|
||
case NE.nonEmpty badNames of
|
||
Just someBadNames -> refute [EnumTableInvalidEnumValueNames someBadNames]
|
||
Nothing -> pure $ HashMap.fromList validEnums
|
||
where
|
||
-- https://graphql.github.io/graphql-spec/June2018/#EnumValue
|
||
mkValidEnumValueName name =
|
||
if name `elem` ["true", "false", "null"]
|
||
then Nothing
|
||
else G.mkName name
|