diff --git a/server/lib/api-tests/src/Test/API/Metadata/LogicalModelsSpec.hs b/server/lib/api-tests/src/Test/API/Metadata/LogicalModelsSpec.hs index 4ea035f83fd..b9d8f8a907a 100644 --- a/server/lib/api-tests/src/Test/API/Metadata/LogicalModelsSpec.hs +++ b/server/lib/api-tests/src/Test/API/Metadata/LogicalModelsSpec.hs @@ -3,6 +3,9 @@ -- | Tests of the Logical Models feature. module Test.API.Metadata.LogicalModelsSpec (spec) where +import Control.Lens +import Data.Aeson qualified as A +import Data.Aeson.Lens import Data.List.NonEmpty qualified as NE import Harness.Backend.BigQuery qualified as BigQuery import Harness.Backend.Citus qualified as Citus @@ -10,13 +13,17 @@ import Harness.Backend.Cockroach qualified as Cockroach import Harness.Backend.Postgres qualified as Postgres import Harness.Backend.Sqlserver qualified as Sqlserver import Harness.GraphqlEngine qualified as GraphqlEngine +import Harness.Quoter.Graphql import Harness.Quoter.Yaml (yaml) import Harness.Quoter.Yaml.InterpolateYaml +import Harness.Services.GraphqlEngine +import Harness.Services.Metadata +import Harness.Services.PostgresSource import Harness.Test.BackendType qualified as BackendType import Harness.Test.Fixture qualified as Fixture import Harness.Test.Schema qualified as Schema import Harness.TestEnvironment (GlobalTestEnvironment, TestEnvironment (options), getBackendTypeConfig, scalarTypeToText) -import Harness.Yaml (shouldReturnYaml) +import Harness.Yaml (shouldBeYaml, shouldReturnYaml) import Hasura.Prelude import Test.Hspec (SpecWith, describe, it) @@ -69,6 +76,8 @@ spec = do (Fixture.runClean fixtures) [testImplementation, testPermissions] + metadataHandlingWhenDisabledSpec + -- ** Setup and teardown schema :: [Schema.Table] @@ -611,3 +620,109 @@ testPermissionFailures = do error: *expectedError path: "$.args" |] + +metadataHandlingWhenDisabledSpec :: SpecWith GlobalTestEnvironment +metadataHandlingWhenDisabledSpec = do + describe "When logical models are enabled" do + withHge + ( emptyHgeConfig + { hgeConfigEnvironmentVars = + [ (featureFlagForLogicalModels, "True") + ] + } + ) + $ withPostgresSource "default" + $ do + it "`replace_metadata` does not report any inconsistent objects" $ \env -> do + currentMetadata <- export_metadata env + actual <- replace_metadata env (metadataWithLogicalModel currentMetadata) + + actual + `shouldBeYaml` [yaml| + inconsistent_objects: [] + is_consistent: true + |] + + it "They do appear in the schema" $ \env -> do + currentMetadata <- export_metadata env + _res <- replace_metadata env (metadataWithLogicalModel currentMetadata) + + let expected = + [yaml| + data: + __type: + name: divided_stuff + |] + + actual <- hgePostGraphql env queryTypesIntrospection + actual `shouldBeYaml` expected + + describe "When logical models are disabled" do + withHge emptyHgeConfig $ do + withPostgresSource "default" $ do + it "`replace_metadata` preserves logical models" $ \env -> do + currentMetadata <- export_metadata env + _ <- replace_metadata env (metadataWithLogicalModel currentMetadata) + actual <- export_metadata env + actual `shouldBeYaml` (metadataWithLogicalModel currentMetadata) + + it "`replace_metadata` reports inconsistent objects" $ \env -> do + currentMetadata <- export_metadata env + actual <- replace_metadata env (metadataWithLogicalModel currentMetadata) + + actual + `shouldBeYaml` [yaml| + inconsistent_objects: + - definition: *logicalModelsMetadata + name: logical_model divided_stuff in source default + reason: 'Inconsistent object: The Logical Models feature is disabled' + type: logical_model + is_consistent: false + |] + + it "They do not appear in the schema" $ \env -> do + currentMetadata <- export_metadata env + _res <- replace_metadata env (metadataWithLogicalModel currentMetadata) + + let expected = + [yaml| + data: + __type: null + |] + + actual <- hgePostGraphql env queryTypesIntrospection + actual `shouldBeYaml` expected + where + logicalModelsMetadata = + [yaml| + arguments: + divided: + nullable: false + type: int + code: SELECT {{divided}} as divided + returns: + columns: + - name: divided + description: a divided thing + nullable: false + type: integer + root_field_name: divided_stuff + |] + + metadataWithLogicalModel :: A.Value -> A.Value + metadataWithLogicalModel currentMetadata = + currentMetadata + & key "sources" + . nth 0 + . atKey "logical_models" + .~ Just [yaml| - *logicalModelsMetadata |] + + queryTypesIntrospection :: A.Value + queryTypesIntrospection = + [graphql| + query { + __type(name: "divided_stuff") { + name + } + } + |] diff --git a/server/lib/test-harness/src/Harness/Services/GraphqlEngine.hs b/server/lib/test-harness/src/Harness/Services/GraphqlEngine.hs index bc6d766369a..07fe11c0353 100644 --- a/server/lib/test-harness/src/Harness/Services/GraphqlEngine.hs +++ b/server/lib/test-harness/src/Harness/Services/GraphqlEngine.hs @@ -9,6 +9,7 @@ module Harness.Services.GraphqlEngine spawnServer, emptyHgeConfig, hgePost, + hgePostGraphql, ) where @@ -280,10 +281,10 @@ hgeLogRelayThread logger hgeOutput = do logParser = json' <* (option () (void (string "\n")) <|> endOfInput) hgePost :: - ( Has HgeServerInstance a, - Has Logger a + ( Has HgeServerInstance env, + Has Logger env ) => - a -> + env -> Int -> Text -> Http.RequestHeaders -> @@ -296,3 +297,13 @@ hgePost env statusCode path headers requestBody = do responseBody <- withFrozenCallStack $ Http.postValueWithStatus statusCode fullUrl headers requestBody testLogMessage env $ LogHGEResponse path responseBody return responseBody + +hgePostGraphql :: + ( Has HgeServerInstance env, + Has Logger env + ) => + env -> + Value -> + IO Value +hgePostGraphql env query = do + hgePost env 200 "/v1/graphql" [] (object ["query" .= query]) diff --git a/server/lib/test-harness/src/Harness/Services/PostgresSource.hs b/server/lib/test-harness/src/Harness/Services/PostgresSource.hs index 2c6a24cf620..1471e07c655 100644 --- a/server/lib/test-harness/src/Harness/Services/PostgresSource.hs +++ b/server/lib/test-harness/src/Harness/Services/PostgresSource.hs @@ -23,11 +23,13 @@ withPostgresSource :: SpecWith (PostgresSource, a) -> SpecWith a withPostgresSource sourceName specs = - flip aroundWith specs \action env -> do - pg_add_source env sourceName - -- TODO assert that res is a success result - -- TODO: use 'managed'? - action (PostgresSource sourceName, env) + -- We mark "Postgres" to interoperate with HASURA_TEST_BACKEND_TYPE. + describe "Postgres" $ + flip aroundWith specs \action env -> do + pg_add_source env sourceName + -- TODO assert that res is a success result + -- TODO: use 'managed'? + action (PostgresSource sourceName, env) pg_add_source :: ( Has Logger env, diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs index 55906cabb86..9452c6810ab 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Cache.hs @@ -92,6 +92,7 @@ import Hasura.SQL.Backend import Hasura.SQL.BackendMap (BackendMap) import Hasura.SQL.BackendMap qualified as BackendMap import Hasura.SQL.Tag +import Hasura.Server.Init.FeatureFlag qualified as FF import Hasura.Server.Migrate.Version import Hasura.Server.Types import Hasura.Services @@ -624,6 +625,7 @@ buildSchemaCacheRule logger env = proc (metadataNoDefaults, serverConfigCtx, inv ArrowKleisli m arr, ArrowWriter (Seq (Either InconsistentMetadata MetadataDependency)) arr, MonadError QErr m, + MonadIO m, BackendMetadata b, GetAggregationPredicatesDeps b ) => @@ -722,30 +724,50 @@ buildSchemaCacheRule logger env = proc (metadataNoDefaults, serverConfigCtx, inv let functionCache = mapFromL _fiSQLName $ catMaybes functionCacheMaybes + areLogicalModelsEnabled <- + bindA + -< do + let CheckFeatureFlag checkFeatureFlag = _sccCheckFeatureFlag serverConfigCtx + liftIO @m $ checkFeatureFlag FF.logicalModelInterface + + let mkLogicalModelMetadataObject :: LogicalModelMetadata b -> MetadataObject + mkLogicalModelMetadataObject lmm = + ( MetadataObject + ( MOSourceObjId sourceName $ + AB.mkAnyBackend $ + SMOLogicalModel @b (_lmmRootFieldName lmm) + ) + (toJSON lmm) + ) + logicalModelCacheMaybes <- interpretWriter -< for (OMap.elems logicalModels) - \LogicalModelMetadata {..} -> runExceptT do - fieldInfoMap <- case toFieldInfo _lmmReturns of - Nothing -> pure mempty - Just fields -> pure (mapFromL fieldInfoName fields) + \lmm@LogicalModelMetadata {..} -> + withRecordInconsistencyM (mkLogicalModelMetadataObject lmm) $ do + unless areLogicalModelsEnabled $ + throw400 InvalidConfiguration "The Logical Models feature is disabled" - logicalModelPermissions <- - buildLogicalModelPermissions sourceName tableCoreInfos _lmmRootFieldName fieldInfoMap _lmmSelectPermissions orderedRoles + fieldInfoMap <- case toFieldInfo _lmmReturns of + Nothing -> pure mempty + Just fields -> pure (mapFromL fieldInfoName fields) - pure - LogicalModelInfo - { _lmiRootFieldName = _lmmRootFieldName, - _lmiCode = _lmmCode, - _lmiReturns = _lmmReturns, - _lmiArguments = _lmmArguments, - _lmiPermissions = logicalModelPermissions, - _lmiDescription = _lmmDescription - } + logicalModelPermissions <- + buildLogicalModelPermissions sourceName tableCoreInfos _lmmRootFieldName fieldInfoMap _lmmSelectPermissions orderedRoles + + pure + LogicalModelInfo + { _lmiRootFieldName = _lmmRootFieldName, + _lmiCode = _lmmCode, + _lmiReturns = _lmmReturns, + _lmiArguments = _lmmArguments, + _lmiPermissions = logicalModelPermissions, + _lmiDescription = _lmmDescription + } let logicalModelCache :: LogicalModelCache b - logicalModelCache = mapFromL _lmiRootFieldName (mapMaybe eitherToMaybe logicalModelCacheMaybes) + logicalModelCache = mapFromL _lmiRootFieldName (catMaybes logicalModelCacheMaybes) returnA -< SourceInfo sourceName tableCache functionCache logicalModelCache sourceConfig queryTagsConfig resolvedCustomization