graphql-engine/server/src-test/Test/Parser/Expectation.hs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

224 lines
8.2 KiB
Haskell
Raw Normal View History

-- | Build expectations for GraphQL field parsers. For now it focuses on updates
-- only.
--
-- See 'runUpdateFieldTest'.
module Test.Parser.Expectation
( UpdateTestSetup (..),
UpdateExpectationBuilder (..),
UpdateVariantBuilder (..),
UpdateBatchBuilder (..),
runUpdateFieldTest,
module I,
AnnotatedUpdateBuilder (..),
mkAnnotatedUpdate,
toBoolExp,
)
where
import Data.HashMap.Strict qualified as HashMap
import Hasura.Backends.Postgres.SQL.Types (QualifiedTable)
import Hasura.Backends.Postgres.Types.Update (PgUpdateVariant (..), UpdateOpExpression (..))
import Hasura.GraphQL.Parser.Internal.Parser (FieldParser (..))
import Hasura.GraphQL.Parser.Schema (Definition (..))
import Hasura.GraphQL.Parser.Variable (Variable (..))
import Hasura.Prelude
import Hasura.RQL.IR.BoolExp (AnnBoolExpFld (..), AnnRedactionExp (..), GBoolExp (..), OpExpG (..))
import Hasura.RQL.IR.Returning (MutationOutputG (..))
import Hasura.RQL.IR.Root (RemoteRelationshipField)
import Hasura.RQL.IR.Update (AnnotatedUpdateG (..))
import Hasura.RQL.IR.Update.Batch (UpdateBatch (..))
import Hasura.RQL.IR.Value (UnpreparedValue)
import Hasura.RQL.Types.BackendType (BackendSourceKind (PostgresVanillaKind), BackendType (Postgres), PostgresKind (Vanilla))
import Hasura.RQL.Types.Column (ColumnInfo (..))
import Hasura.RQL.Types.Common (ResolvedWebhook, SourceName (..))
import Hasura.RQL.Types.NamingCase
import Hasura.RQL.Types.Permission
import Hasura.RQL.Types.Source (DBObjectsIntrospection (..), SourceInfo (..))
import Hasura.RQL.Types.SourceCustomization (ResolvedSourceCustomization (..))
import Hasura.Table.Cache (TableInfo (..))
import Language.GraphQL.Draft.Syntax qualified as Syntax
import Test.Hspec
import Test.Parser.Internal
import Test.Parser.Internal as I (ColumnInfoBuilder (..), mkColumnInfo, mkTable)
import Test.Parser.Monad
type PG = 'Postgres 'Vanilla
type BoolExp = GBoolExp PG (AnnBoolExpFld PG (UnpreparedValue PG))
type Output r = MutationOutputG PG r (UnpreparedValue PG)
type Field = Syntax.Field Syntax.NoFragments Variable
type Update = UpdateVariantBuilder ColumnInfoBuilder
-- | Holds all the information required to setup and run a field parser update
-- test.
data UpdateTestSetup = UpdateTestSetup
{ -- | name of the table
utsTable :: Text,
-- | table columns
utsColumns :: [ColumnInfoBuilder],
-- | expectation
utsExpect :: UpdateExpectationBuilder,
-- | GrqphQL field, see Test.Parser.Parser
utsField :: Field
}
-- | Build the expected output columns, where and update clauses.
data UpdateExpectationBuilder = UpdateExpectationBuilder
{ -- | build the expected selection set/output, e.g.
--
-- > MOutMultirowFields [("affected_rows", MCount)]
utbOutput :: Output (RemoteRelationshipFieldWrapper UnpreparedValue),
-- | expected update clause(s), including the where condition as update operations,
-- e.g. given a @nameColumn :: ColumnInfoBuilder@ and
-- @newValue :: UnpreparedValue PG@:
--
-- > SingleBatchUpdate (UpdateBatchBuilder [(nameColumn, [AEQ true oldvalue])] [(nameColumn, UpdateSet newValue)])
utbUpdate :: Update
}
-- | Run a test given the schema and field.
runUpdateFieldTest :: UpdateTestSetup -> Expectation
runUpdateFieldTest UpdateTestSetup {..} =
case runSchemaTest sourceInfo $ mkParser ((tableInfoBuilder table) {columns = utsColumns}) of
[] -> expectationFailure "expected at least one parser"
parsers ->
case find (byName (Syntax._fName utsField)) parsers of
Nothing -> expectationFailure $ "could not find parser " <> show (Syntax._fName utsField)
Just FieldParser {..} -> do
annUpdate <- runParserTest $ fParser utsField
coerce annUpdate `shouldBe` expected
where
UpdateExpectationBuilder {..} = utsExpect
sourceInfo :: SourceInfo ('Postgres 'Vanilla)
sourceInfo =
SourceInfo
{ _siName = SNDefault,
_siSourceKind = PostgresVanillaKind,
_siTables = HashMap.singleton table tableInfo,
_siFunctions = mempty,
_siNativeQueries = mempty,
_siStoredProcedures = mempty,
_siLogicalModels = mempty,
_siConfiguration = notImplementedYet "SourceConfig",
_siQueryTagsConfig = Nothing,
_siCustomization = ResolvedSourceCustomization mempty mempty HasuraCase Nothing,
Replace TableObjectType, etc. with the corresponding Logical Model types ## Description This is the first step in making use of Logical Models with document databases such as MongoDB. As part of schema introspection, a data connector agent can supply a set of custom types that can be used to describe the schema for columns within the tables of the database (or _fields_ within a _document collection_ in MongoDB terminology). Previously, we were storing these custom types as `TableObjectType`s within the `TableCoreInfo` for each table. In this PR we - replace the `TableObjectTypes` with `LogicalModel` types - store these directly within the `DBObjectsIntrospection` instead of within the `TableCoreInfo` for each table. (The custom types are shared at the source level so there was no reason to have a separate set of types for each table.) - When building the `SourceInfo`, we combine the `LogicalModel`s from `DBObjectsIntrospection` with `LogicalModel`s from the user's metadata to create the set of `LogicalModels` in the `SourceInfo` within the `SchemaCache`. I.e. we combine the set of types obtained by database introspection with the set of types specified by the user in the metadata. If two types have the same name, we use the type defined in the metadata. ## Limitations and future work - Provide a way for the user to associate a meta-data defined `LogicalModel` with a table instead of requiring one to be provided by DB introspection - Provide a way for the user to edit the `LogicalModel` types provided by introspection and add them to the metadata. - Allow a `LogicalModel` object type to describe and entire table rather than just individual columns. - Better handling for "unknown" types, e.g. if the type of a collection (or part of a collection) is unknown we should treat it as a JSON scalar value. This may also involve adding an `_everything` field which returns the full document as a JSON scalar. PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9345 GitOrigin-RevId: 5cec72fc1be1380d8600f7be547bbf71aad770bd
2023-05-30 17:04:16 +03:00
_siDbObjectsIntrospection = DBObjectsIntrospection mempty mempty mempty mempty
}
byName :: Syntax.Name -> Parser -> Bool
byName name FieldParser {..} = name == dName fDefinition
table :: QualifiedTable
table = mkTable utsTable
tableInfo :: TableInfo PG
tableInfo =
buildTableInfo
TableInfoBuilder
{ table = table,
columns = utsColumns,
relations = []
}
expected :: AnnotatedUpdateG PG (RemoteRelationshipFieldWrapper UnpreparedValue) (UnpreparedValue PG)
expected =
mkAnnotatedUpdate
AnnotatedUpdateBuilder
{ aubTable = table,
aubOutput = utbOutput,
aubColumns = mkColumnInfo <$> utsColumns,
aubUpdateVariant = mkUpdateColumns utbUpdate
}
mkUpdateColumns :: UpdateVariantBuilder ColumnInfoBuilder -> UpdateVariantBuilder (ColumnInfo PG)
mkUpdateColumns = fmap mkColumnInfo
-- | Internal use only. The intended use is through 'runUpdateFieldTest'.
--
-- Build an 'AnnotatedUpdateG', to be used with 'mkAnnotatedUpdate'.
data AnnotatedUpdateBuilder r = AnnotatedUpdateBuilder
{ -- | the main table for the update
aubTable :: QualifiedTable,
-- | the 'Output' clause, e.g., selection set, affected_rows, etc.
aubOutput :: Output r,
-- | the table columns (all of them)
aubColumns :: [ColumnInfo PG],
-- | the update statement(s)
aubUpdateVariant :: UpdateVariantBuilder (ColumnInfo PG)
}
data UpdateVariantBuilder col
= SingleBatchUpdate (UpdateBatchBuilder col)
| MultipleBatchesUpdate [UpdateBatchBuilder col]
deriving stock (Functor)
data UpdateBatchBuilder col = UpdateBatchBuilder
{ ubbWhere :: [(col, [OpExpG PG (UnpreparedValue PG)])],
ubbOperations :: [(col, UpdateOpExpression (UnpreparedValue PG))]
}
deriving stock (Functor)
-- | 'RemoteRelationshipField' cannot have Eq/Show instances, so we're wrapping
-- it.
newtype RemoteRelationshipFieldWrapper vf = RemoteRelationshipFieldWrapper (RemoteRelationshipField vf)
instance Show (RemoteRelationshipFieldWrapper vf) where
show =
error "Test.Parser.Expectation: no Show implementation for RemoteRelationshipFieldWrapper"
instance Eq (RemoteRelationshipFieldWrapper vf) where
(==) =
error "Test.Parser.Expectation: no Eq implementation for RemoteRelationshipFieldWrapper"
-- | Internal use, see 'runUpdateFieldTest'.
mkAnnotatedUpdate ::
forall r.
AnnotatedUpdateBuilder r ->
AnnotatedUpdateG PG r (UnpreparedValue PG)
mkAnnotatedUpdate AnnotatedUpdateBuilder {..} = AnnotatedUpdateG {..}
where
_auTable :: QualifiedTable
_auTable = aubTable
_auCheck :: BoolExp
_auCheck = BoolAnd []
_auUpdateVariant :: PgUpdateVariant 'Vanilla (UnpreparedValue PG)
_auUpdateVariant =
case aubUpdateVariant of
SingleBatchUpdate batch ->
SingleBatch $ mapUpdateBatch batch
MultipleBatchesUpdate batches ->
MultipleBatches $ mapUpdateBatch <$> batches
mapUpdateBatch :: UpdateBatchBuilder (ColumnInfo PG) -> UpdateBatch ('Postgres 'Vanilla) UpdateOpExpression (UnpreparedValue PG)
mapUpdateBatch UpdateBatchBuilder {..} =
UpdateBatch
{ _ubWhere = toBoolExp ubbWhere,
_ubOperations = HashMap.fromList $ fmap (first ciColumn) ubbOperations
}
_auOutput :: Output r
_auOutput = aubOutput
_auAllCols :: [ColumnInfo PG]
_auAllCols = aubColumns
_auUpdatePermissions :: BoolExp
_auUpdatePermissions =
BoolAnd
. fmap (\c -> BoolField . AVColumn c NoRedaction $ [])
$ aubColumns
_auNamingConvention :: Maybe NamingCase
_auNamingConvention = Just HasuraCase
_auValidateInput :: Maybe (ValidateInput ResolvedWebhook)
_auValidateInput = Nothing
toBoolExp :: [(ColumnInfo PG, [OpExpG PG (UnpreparedValue PG)])] -> BoolExp
toBoolExp = BoolAnd . fmap (\(c, ops) -> BoolField $ AVColumn c NoRedaction ops)