mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 08:02:15 +03:00
Enable and test remote relationships from remote schemas.
### Description This is it! This PR enables the Metadata API for remote relationships from remote schemas, adds tests, ~~adds documentation~~, adds an entry to the Changelog. This is the release PR that enables the feature. ### Checklist - [ ] Tests: - [x] RS-to-Postgres (high level) - [x] RS-to-RS (high level) - [x] From RS specifically (testing for edge cases) - [x] Metadata API tests - [ ] Unit testing the actual engine? - [x] Changelog entry - [ ] Documentation? PR-URL: https://github.com/hasura/graphql-engine-mono/pull/3974 Co-authored-by: Vamshi Surabhi <6562944+0x777@users.noreply.github.com> Co-authored-by: Vishnu Bharathi <4211715+scriptnull@users.noreply.github.com> Co-authored-by: jkachmar <8461423+jkachmar@users.noreply.github.com> GitOrigin-RevId: c9aebf12e6eebef8d264ea831a327b968d4be9d2
This commit is contained in:
parent
810c94c776
commit
ccea1da1d5
44
CHANGELOG.md
44
CHANGELOG.md
@ -1,6 +1,50 @@
|
|||||||
# Hasura GraphQL Engine Changelog
|
# Hasura GraphQL Engine Changelog
|
||||||
|
|
||||||
## Next release
|
## Next release
|
||||||
|
|
||||||
|
### Remote relationships from remote schemas
|
||||||
|
|
||||||
|
This release adds three new metadata API commands:
|
||||||
|
- `create_remote_schema_remote_relationship`
|
||||||
|
- `update_remote_schema_remote_relationship`
|
||||||
|
- `delete_remote_schema_remote_relationship`
|
||||||
|
|
||||||
|
that allows to create remote relationships between remote schemas on
|
||||||
|
the left-hand side and databases or remote schemas on the right-hand
|
||||||
|
side. Both use the same syntax as remote relationships from databases:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: LeftHandSide
|
||||||
|
type_name: LeftHandSideTypeName
|
||||||
|
name: RelationshipName
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: RightHandSideSchema
|
||||||
|
lhs_fields: [LHSJoinKeyName]
|
||||||
|
remote_field:
|
||||||
|
rhsFieldName:
|
||||||
|
arguments:
|
||||||
|
ids: $LHSJoinKeyName
|
||||||
|
|
||||||
|
type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: LeftHandSide
|
||||||
|
type_name: LeftHandSideTypeName
|
||||||
|
name: RelationshipName
|
||||||
|
definition:
|
||||||
|
to_source:
|
||||||
|
source: RightHandSideSource
|
||||||
|
table: {schema: public, name: RHSTable}
|
||||||
|
relationship_type: object
|
||||||
|
field_mapping:
|
||||||
|
LHSJoinKeyName: RHSColumnName
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly to DB-to-DB relationships, only `Postgres` is supported on
|
||||||
|
the right-hand side for now.
|
||||||
|
|
||||||
### Deprecations
|
### Deprecations
|
||||||
* The `custom_column_names` property of TableConfig used on `<db>_track_table` and `set_table_customization` metadata APIs has been deprecated in favour of the new `column_config` property. `custom_column_names` will still work for now, however, values used in `column_config` will take precedence over values from `custom_column_names` and any overlapped values in `custom_column_names` will be discarded.
|
* The `custom_column_names` property of TableConfig used on `<db>_track_table` and `set_table_customization` metadata APIs has been deprecated in favour of the new `column_config` property. `custom_column_names` will still work for now, however, values used in `column_config` will take precedence over values from `custom_column_names` and any overlapped values in `custom_column_names` will be discarded.
|
||||||
|
|
||||||
|
@ -1047,6 +1047,7 @@ test-suite tests-hspec
|
|||||||
Test.ObjectRelationshipsLimitSpec
|
Test.ObjectRelationshipsLimitSpec
|
||||||
Test.ObjectRelationshipsSpec
|
Test.ObjectRelationshipsSpec
|
||||||
Test.OrderingSpec
|
Test.OrderingSpec
|
||||||
|
Test.RemoteRelationship.FromRemoteSchemaSpec
|
||||||
Test.RemoteRelationship.XToDBArrayRelationshipSpec
|
Test.RemoteRelationship.XToDBArrayRelationshipSpec
|
||||||
Test.RemoteRelationship.XToDBObjectRelationshipSpec
|
Test.RemoteRelationship.XToDBObjectRelationshipSpec
|
||||||
Test.RemoteRelationship.XToRemoteSchemaRelationshipSpec
|
Test.RemoteRelationship.XToRemoteSchemaRelationshipSpec
|
||||||
|
@ -364,7 +364,7 @@ getRemoteSchemaEntityJoinColumns ::
|
|||||||
getRemoteSchemaEntityJoinColumns remoteSchemaName introspection typeName = do
|
getRemoteSchemaEntityJoinColumns remoteSchemaName introspection typeName = do
|
||||||
typeDefinition <-
|
typeDefinition <-
|
||||||
onNothing (lookupType introspection typeName) $
|
onNothing (lookupType introspection typeName) $
|
||||||
throw400 NotFound ("no such type defined in remote schema: " <>> remoteSchemaName)
|
throw400 NotFound ("no type named " <> typeName <<> " defined in remote schema " <>> remoteSchemaName)
|
||||||
case typeDefinition of
|
case typeDefinition of
|
||||||
G.TypeDefinitionObject objectDefinition ->
|
G.TypeDefinitionObject objectDefinition ->
|
||||||
pure $
|
pure $
|
||||||
|
@ -112,6 +112,10 @@ data RQLMetadataV1
|
|||||||
| -- Remote schemas permissions
|
| -- Remote schemas permissions
|
||||||
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
||||||
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
||||||
|
| -- Remote Schema remote relationships
|
||||||
|
RMCreateRemoteSchemaRemoteRelationship CreateRemoteSchemaRemoteRelationship
|
||||||
|
| RMUpdateRemoteSchemaRemoteRelationship CreateRemoteSchemaRemoteRelationship
|
||||||
|
| RMDeleteRemoteSchemaRemoteRelationship DeleteRemoteSchemaRemoteRelationship
|
||||||
| -- Scheduled triggers
|
| -- Scheduled triggers
|
||||||
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
||||||
| RMDeleteCronTrigger !ScheduledTriggerName
|
| RMDeleteCronTrigger !ScheduledTriggerName
|
||||||
@ -185,6 +189,9 @@ instance FromJSON RQLMetadataV1 where
|
|||||||
"introspect_remote_schema" -> RMIntrospectRemoteSchema <$> args
|
"introspect_remote_schema" -> RMIntrospectRemoteSchema <$> args
|
||||||
"add_remote_schema_permissions" -> RMAddRemoteSchemaPermissions <$> args
|
"add_remote_schema_permissions" -> RMAddRemoteSchemaPermissions <$> args
|
||||||
"drop_remote_schema_permissions" -> RMDropRemoteSchemaPermissions <$> args
|
"drop_remote_schema_permissions" -> RMDropRemoteSchemaPermissions <$> args
|
||||||
|
"create_remote_schema_remote_relationship" -> RMCreateRemoteSchemaRemoteRelationship <$> args
|
||||||
|
"update_remote_schema_remote_relationship" -> RMUpdateRemoteSchemaRemoteRelationship <$> args
|
||||||
|
"delete_remote_schema_remote_relationship" -> RMDeleteRemoteSchemaRemoteRelationship <$> args
|
||||||
"create_cron_trigger" -> RMCreateCronTrigger <$> args
|
"create_cron_trigger" -> RMCreateCronTrigger <$> args
|
||||||
"delete_cron_trigger" -> RMDeleteCronTrigger <$> args
|
"delete_cron_trigger" -> RMDeleteCronTrigger <$> args
|
||||||
"create_scheduled_event" -> RMCreateScheduledEvent <$> args
|
"create_scheduled_event" -> RMCreateScheduledEvent <$> args
|
||||||
@ -317,6 +324,7 @@ runMetadataQuery env logger instanceId userInfo httpManager serverConfigCtx sche
|
|||||||
(MaintenanceModeDisabled, ReadOnlyModeDisabled) -> do
|
(MaintenanceModeDisabled, ReadOnlyModeDisabled) -> do
|
||||||
-- set modified metadata in storage
|
-- set modified metadata in storage
|
||||||
newResourceVersion <- setMetadata (fromMaybe currentResourceVersion _rqlMetadataResourceVersion) modMetadata
|
newResourceVersion <- setMetadata (fromMaybe currentResourceVersion _rqlMetadataResourceVersion) modMetadata
|
||||||
|
|
||||||
-- notify schema cache sync
|
-- notify schema cache sync
|
||||||
notifySchemaCacheSync newResourceVersion instanceId cacheInvalidations
|
notifySchemaCacheSync newResourceVersion instanceId cacheInvalidations
|
||||||
(_, modSchemaCache', _) <-
|
(_, modSchemaCache', _) <-
|
||||||
@ -449,6 +457,9 @@ runMetadataQueryV1M env currentResourceVersion = \case
|
|||||||
RMIntrospectRemoteSchema q -> runIntrospectRemoteSchema q
|
RMIntrospectRemoteSchema q -> runIntrospectRemoteSchema q
|
||||||
RMAddRemoteSchemaPermissions q -> runAddRemoteSchemaPermissions q
|
RMAddRemoteSchemaPermissions q -> runAddRemoteSchemaPermissions q
|
||||||
RMDropRemoteSchemaPermissions q -> runDropRemoteSchemaPermissions q
|
RMDropRemoteSchemaPermissions q -> runDropRemoteSchemaPermissions q
|
||||||
|
RMCreateRemoteSchemaRemoteRelationship q -> runCreateRemoteSchemaRemoteRelationship q
|
||||||
|
RMUpdateRemoteSchemaRemoteRelationship q -> runUpdateRemoteSchemaRemoteRelationship q
|
||||||
|
RMDeleteRemoteSchemaRemoteRelationship q -> runDeleteRemoteSchemaRemoteRelationship q
|
||||||
RMCreateCronTrigger q ->
|
RMCreateCronTrigger q ->
|
||||||
validateTransforms
|
validateTransforms
|
||||||
(unUnvalidate . cctRequestTransform . _Just)
|
(unUnvalidate . cctRequestTransform . _Just)
|
||||||
|
@ -74,6 +74,10 @@ data RQLMetadataV1
|
|||||||
| -- Remote schemas permissions
|
| -- Remote schemas permissions
|
||||||
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
RMAddRemoteSchemaPermissions !AddRemoteSchemaPermission
|
||||||
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
| RMDropRemoteSchemaPermissions !DropRemoteSchemaPermissions
|
||||||
|
| -- Remote Schema remote relationships
|
||||||
|
RMCreateRemoteSchemaRemoteRelationship CreateRemoteSchemaRemoteRelationship
|
||||||
|
| RMUpdateRemoteSchemaRemoteRelationship CreateRemoteSchemaRemoteRelationship
|
||||||
|
| RMDeleteRemoteSchemaRemoteRelationship DeleteRemoteSchemaRemoteRelationship
|
||||||
| -- Scheduled triggers
|
| -- Scheduled triggers
|
||||||
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
RMCreateCronTrigger !(Unvalidated CreateCronTrigger)
|
||||||
| RMDeleteCronTrigger !ScheduledTriggerName
|
| RMDeleteCronTrigger !ScheduledTriggerName
|
||||||
|
@ -263,7 +263,7 @@ serveOptions =
|
|||||||
-- only used in 'serveOptions'.
|
-- only used in 'serveOptions'.
|
||||||
--
|
--
|
||||||
-- This should be adjusted locally for debugging purposes; e.g. change it to
|
-- This should be adjusted locally for debugging purposes; e.g. change it to
|
||||||
-- @Just L.Debug@ to enable all logs.
|
-- @Just L.LevelDebug@ to enable all logs.
|
||||||
--
|
--
|
||||||
-- See 'L.LogLevel' for an enumeration of available log levels.
|
-- See 'L.LogLevel' for an enumeration of available log levels.
|
||||||
engineLogLevel :: Maybe L.LogLevel
|
engineLogLevel :: Maybe L.LogLevel
|
||||||
|
@ -21,12 +21,8 @@ module Harness.GraphqlEngine
|
|||||||
setSource,
|
setSource,
|
||||||
setSources,
|
setSources,
|
||||||
|
|
||||||
-- * Misc. Helpers
|
-- * Server Setup
|
||||||
graphqlEndpoint,
|
|
||||||
|
|
||||||
-- * Server Setup & Teardown
|
|
||||||
startServerThread,
|
startServerThread,
|
||||||
stopServer,
|
|
||||||
|
|
||||||
-- * Re-exports
|
-- * Re-exports
|
||||||
serverUrl,
|
serverUrl,
|
||||||
@ -36,7 +32,7 @@ where
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
import Control.Concurrent (forkIO, killThread, threadDelay)
|
import Control.Concurrent (forkIO, threadDelay)
|
||||||
import Control.Exception.Safe (bracket)
|
import Control.Exception.Safe (bracket)
|
||||||
import Control.Monad.Trans.Managed (ManagedT (..), lowerManagedT)
|
import Control.Monad.Trans.Managed (ManagedT (..), lowerManagedT)
|
||||||
import Data.Aeson (Value, object, (.=))
|
import Data.Aeson (Value, object, (.=))
|
||||||
@ -85,7 +81,7 @@ post_ state path = void . postWithHeaders_ state path mempty
|
|||||||
postWithHeaders ::
|
postWithHeaders ::
|
||||||
HasCallStack => State -> String -> Http.RequestHeaders -> Value -> IO Value
|
HasCallStack => State -> String -> Http.RequestHeaders -> Value -> IO Value
|
||||||
postWithHeaders (getServer -> Server {urlPrefix, port}) path =
|
postWithHeaders (getServer -> Server {urlPrefix, port}) path =
|
||||||
Http.postValue_ (urlPrefix ++ ":" ++ show port ++ path)
|
Http.postValue (urlPrefix ++ ":" ++ show port ++ path)
|
||||||
|
|
||||||
-- | Post some JSON to graphql-engine, getting back more JSON.
|
-- | Post some JSON to graphql-engine, getting back more JSON.
|
||||||
--
|
--
|
||||||
@ -150,17 +146,6 @@ args:
|
|||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- | Extracts the full GraphQL endpoint URL from a given 'Server'.
|
|
||||||
--
|
|
||||||
-- @
|
|
||||||
-- > graphqlEndpoint (Server 8080 "http://localhost" someThreadId)
|
|
||||||
-- "http://localhost:8080/graphql"
|
|
||||||
-- @
|
|
||||||
graphqlEndpoint :: Server -> String
|
|
||||||
graphqlEndpoint server = serverUrl server ++ "/graphql"
|
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
-- | Choose a random port and start a graphql-engine server on that
|
-- | Choose a random port and start a graphql-engine server on that
|
||||||
-- port accessible from localhost. It waits until the server is
|
-- port accessible from localhost. It waits until the server is
|
||||||
-- available before returning.
|
-- available before returning.
|
||||||
@ -183,10 +168,6 @@ startServerThread murlPrefixport = do
|
|||||||
Http.healthCheck (serverUrl server)
|
Http.healthCheck (serverUrl server)
|
||||||
pure server
|
pure server
|
||||||
|
|
||||||
-- | Forcibly stop a given 'Server'.
|
|
||||||
stopServer :: Server -> IO ()
|
|
||||||
stopServer Server {threadId} = killThread threadId
|
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- | Run the graphql-engine server.
|
-- | Run the graphql-engine server.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
-- | Helper functions for HTTP requests.
|
-- | Helper functions for HTTP requests.
|
||||||
module Harness.Http
|
module Harness.Http
|
||||||
( get_,
|
( get_,
|
||||||
postValue_,
|
postValue,
|
||||||
healthCheck,
|
healthCheck,
|
||||||
Http.RequestHeaders,
|
Http.RequestHeaders,
|
||||||
)
|
)
|
||||||
@ -32,8 +32,8 @@ get_ url = do
|
|||||||
|
|
||||||
-- | Post the JSON to the given URL, and produces a very descriptive
|
-- | Post the JSON to the given URL, and produces a very descriptive
|
||||||
-- exception on failure.
|
-- exception on failure.
|
||||||
postValue_ :: HasCallStack => String -> Http.RequestHeaders -> Value -> IO Value
|
postValue :: HasCallStack => String -> Http.RequestHeaders -> Value -> IO Value
|
||||||
postValue_ url headers value = do
|
postValue url headers value = do
|
||||||
let request =
|
let request =
|
||||||
Http.setRequestHeaders headers $
|
Http.setRequestHeaders headers $
|
||||||
Http.setRequestMethod Http.methodPost $
|
Http.setRequestMethod Http.methodPost $
|
||||||
|
@ -3,6 +3,7 @@ module Harness.RemoteServer
|
|||||||
( run,
|
( run,
|
||||||
generateInterpreter,
|
generateInterpreter,
|
||||||
generateQueryInterpreter,
|
generateQueryInterpreter,
|
||||||
|
graphqlEndpoint,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@ -162,6 +163,18 @@ generateQueryInterpreter ::
|
|||||||
Interpreter
|
Interpreter
|
||||||
generateQueryInterpreter queryResolver = generateInterpreter queryResolver Undefined
|
generateQueryInterpreter queryResolver = generateInterpreter queryResolver Undefined
|
||||||
|
|
||||||
|
-- | Extracts the full GraphQL endpoint URL from a given remote server's 'Server'.
|
||||||
|
--
|
||||||
|
-- @
|
||||||
|
-- > graphqlEndpoint (Server 8080 "http://localhost" someThreadId)
|
||||||
|
-- "http://localhost:8080/graphql"
|
||||||
|
-- @
|
||||||
|
--
|
||||||
|
-- NOTE: the resulting endpoint is only relevant for a 'Server' started by this
|
||||||
|
-- module's 'run' function; the GraphQL engine doesn't have a /graphql endoint.
|
||||||
|
graphqlEndpoint :: Server -> String
|
||||||
|
graphqlEndpoint server = serverUrl server ++ "/graphql"
|
||||||
|
|
||||||
-------------------------------------------------------------------------------
|
-------------------------------------------------------------------------------
|
||||||
|
|
||||||
-- | An interpreter is a transformation function, applied to an incoming GraphQL
|
-- | An interpreter is a transformation function, applied to an incoming GraphQL
|
||||||
|
@ -7,10 +7,11 @@ module Harness.State
|
|||||||
Server (..),
|
Server (..),
|
||||||
getServer,
|
getServer,
|
||||||
serverUrl,
|
serverUrl,
|
||||||
|
stopServer,
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Concurrent
|
import Control.Concurrent (ThreadId, killThread)
|
||||||
import Data.Word
|
import Data.Word
|
||||||
import Hasura.Prelude hiding (State)
|
import Hasura.Prelude hiding (State)
|
||||||
|
|
||||||
@ -41,3 +42,7 @@ getServer State {server} = server
|
|||||||
-- @
|
-- @
|
||||||
serverUrl :: Server -> String
|
serverUrl :: Server -> String
|
||||||
serverUrl Server {urlPrefix, port} = urlPrefix ++ ":" ++ show port
|
serverUrl Server {urlPrefix, port} = urlPrefix ++ ":" ++ show port
|
||||||
|
|
||||||
|
-- | Forcibly stop a given 'Server'.
|
||||||
|
stopServer :: Server -> IO ()
|
||||||
|
stopServer Server {threadId} = killThread threadId
|
||||||
|
@ -191,10 +191,12 @@ data Context a = Context
|
|||||||
-- | A name describing the given context.
|
-- | A name describing the given context.
|
||||||
data ContextName
|
data ContextName
|
||||||
= Backend BackendType
|
= Backend BackendType
|
||||||
|
| RemoteGraphQLServer
|
||||||
| Combine ContextName ContextName
|
| Combine ContextName ContextName
|
||||||
|
|
||||||
instance Show ContextName where
|
instance Show ContextName where
|
||||||
show (Backend backend) = show backend
|
show (Backend backend) = show backend
|
||||||
|
show RemoteGraphQLServer = "RemoteGraphQLServer"
|
||||||
show (Combine name1 name2) = show name1 ++ "-" ++ show name2
|
show (Combine name1 name2) = show name1 ++ "-" ++ show name2
|
||||||
|
|
||||||
-- | Default function for 'mkLocalState' when there's no local state.
|
-- | Default function for 'mkLocalState' when there's no local state.
|
||||||
|
@ -4,8 +4,8 @@ module SpecHook
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Control.Exception.Safe (bracket)
|
import Control.Exception.Safe (bracket)
|
||||||
import Harness.GraphqlEngine (startServerThread, stopServer)
|
import Harness.GraphqlEngine (startServerThread)
|
||||||
import Harness.State (State (..))
|
import Harness.State (State (..), stopServer)
|
||||||
import System.Environment (lookupEnv)
|
import System.Environment (lookupEnv)
|
||||||
import Test.Hspec (Spec, SpecWith, aroundAllWith)
|
import Test.Hspec (Spec, SpecWith, aroundAllWith)
|
||||||
import Text.Read (readMaybe)
|
import Text.Read (readMaybe)
|
||||||
|
@ -0,0 +1,747 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
|
-- | Tests for remote relationships from remote schemas. Unlike the "ToX"
|
||||||
|
-- modules, this module specifically cares about the remote schema on the LHS:
|
||||||
|
-- - testing the metadata API
|
||||||
|
-- - testing how we generate queries to remote schemas for the purpose of
|
||||||
|
-- making joins against them.
|
||||||
|
module Test.RemoteRelationship.FromRemoteSchemaSpec (spec) where
|
||||||
|
|
||||||
|
import Data.Functor ((<&>))
|
||||||
|
import Data.Morpheus.Document (gqlDocument)
|
||||||
|
import Data.Morpheus.Types
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Harness.Backend.Postgres qualified as Postgres
|
||||||
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
||||||
|
import Harness.Quoter.Graphql (graphql)
|
||||||
|
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
||||||
|
import Harness.RemoteServer qualified as RemoteServer
|
||||||
|
import Harness.State (Server, State, stopServer)
|
||||||
|
import Harness.Test.Context (Context (..))
|
||||||
|
import Harness.Test.Context qualified as Context
|
||||||
|
import Harness.Test.Schema qualified as Schema
|
||||||
|
import Test.Hspec (SpecWith, describe, it)
|
||||||
|
import Prelude
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Preamble
|
||||||
|
|
||||||
|
spec :: SpecWith State
|
||||||
|
spec = Context.runWithLocalState [context] tests
|
||||||
|
where
|
||||||
|
context =
|
||||||
|
Context
|
||||||
|
{ name = Context.RemoteGraphQLServer,
|
||||||
|
-- start only one remote server
|
||||||
|
mkLocalState = \_state ->
|
||||||
|
RemoteServer.run $
|
||||||
|
RemoteServer.generateQueryInterpreter $
|
||||||
|
Query
|
||||||
|
{ object = objectResolver,
|
||||||
|
writer = writerResolver,
|
||||||
|
artist = artistResolver,
|
||||||
|
objects = objectsResolver,
|
||||||
|
articles = articlesResolver
|
||||||
|
},
|
||||||
|
-- set that remote server as both source and target, for convenience
|
||||||
|
-- start a RHS Postgres for Metadata tests only
|
||||||
|
setup = \(state, server) -> do
|
||||||
|
GraphqlEngine.clearMetadata state
|
||||||
|
addRemoteSchema state "remote" server
|
||||||
|
addRelationships state
|
||||||
|
rhsPostgresSetup state,
|
||||||
|
-- shutdown the server
|
||||||
|
teardown = \(state, server) -> do
|
||||||
|
GraphqlEngine.clearMetadata state
|
||||||
|
stopServer server
|
||||||
|
rhsPostgresTeardown,
|
||||||
|
-- no custom options
|
||||||
|
customOptions = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | Add a remote schema to the engine with the given name.
|
||||||
|
addRemoteSchema :: State -> String -> Server -> IO ()
|
||||||
|
addRemoteSchema state rsName remoteServer = do
|
||||||
|
let remoteSchemaEndpoint = RemoteServer.graphqlEndpoint remoteServer
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: add_remote_schema
|
||||||
|
args:
|
||||||
|
name: *rsName
|
||||||
|
definition:
|
||||||
|
url: *remoteSchemaEndpoint
|
||||||
|
|]
|
||||||
|
|
||||||
|
-- | Create the remote relationships.
|
||||||
|
addRelationships :: State -> IO ()
|
||||||
|
addRelationships state = do
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: articles
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [wIds]
|
||||||
|
remote_field:
|
||||||
|
articles:
|
||||||
|
arguments:
|
||||||
|
ids: $wIds
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Artist
|
||||||
|
name: articles
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [aIds]
|
||||||
|
remote_field:
|
||||||
|
articles:
|
||||||
|
arguments:
|
||||||
|
ids: $aIds
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Article
|
||||||
|
name: artist
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [aId]
|
||||||
|
remote_field:
|
||||||
|
artist:
|
||||||
|
arguments:
|
||||||
|
id: $aId
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Article
|
||||||
|
name: writer
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [wId]
|
||||||
|
remote_field:
|
||||||
|
writer:
|
||||||
|
arguments:
|
||||||
|
id: $wId
|
||||||
|
|]
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Remote schema
|
||||||
|
|
||||||
|
[gqlDocument|
|
||||||
|
|
||||||
|
type Query {
|
||||||
|
object(id: Int!): Object
|
||||||
|
writer(id: Int!): Writer
|
||||||
|
artist(id: Int!): Artist
|
||||||
|
objects(ids: [Int!]!): [Object]!
|
||||||
|
articles(ids: [Int!]!): [Article]!
|
||||||
|
}
|
||||||
|
|
||||||
|
union Object = Writer | Artist | Article
|
||||||
|
|
||||||
|
type Writer {
|
||||||
|
id: Int!
|
||||||
|
name: String!
|
||||||
|
wIds: [Int!]!
|
||||||
|
local_articles: [Article!]!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Artist {
|
||||||
|
id: Int!
|
||||||
|
name: String!
|
||||||
|
aIds: [Int!]!
|
||||||
|
local_articles: [Article!]!
|
||||||
|
self: Artist!
|
||||||
|
}
|
||||||
|
|
||||||
|
type Article {
|
||||||
|
id: Int!
|
||||||
|
title: String!
|
||||||
|
aId: Int!
|
||||||
|
wId: Int!
|
||||||
|
}
|
||||||
|
|
||||||
|
|]
|
||||||
|
|
||||||
|
knownObjects :: Monad m => [(Int, Object m)]
|
||||||
|
knownObjects =
|
||||||
|
[ (101, ObjectWriter writer1),
|
||||||
|
(102, ObjectWriter writer2),
|
||||||
|
(201, ObjectArtist artist1),
|
||||||
|
(202, ObjectArtist artist2),
|
||||||
|
(301, ObjectArticle article1),
|
||||||
|
(302, ObjectArticle article2),
|
||||||
|
(303, ObjectArticle article3),
|
||||||
|
(304, ObjectArticle article4)
|
||||||
|
]
|
||||||
|
where
|
||||||
|
writer1 = Writer (pure 101) (pure "Writer1") (pure [301, 302]) (pure [article1, article2])
|
||||||
|
writer2 = Writer (pure 102) (pure "Writer2") (pure [303, 304]) (pure [article3, article4])
|
||||||
|
artist1 = Artist (pure 201) (pure "Artist1") (pure [301, 303]) (pure [article1, article3]) (pure artist1)
|
||||||
|
artist2 = Artist (pure 202) (pure "Artist2") (pure [302, 304]) (pure [article2, article4]) (pure artist2)
|
||||||
|
article1 = Article (pure 301) (pure "Article1") (pure 201) (pure 101)
|
||||||
|
article2 = Article (pure 302) (pure "Article2") (pure 202) (pure 101)
|
||||||
|
article3 = Article (pure 303) (pure "Article3") (pure 201) (pure 102)
|
||||||
|
article4 = Article (pure 304) (pure "Article4") (pure 202) (pure 102)
|
||||||
|
|
||||||
|
objectResolver :: Monad m => Arg "id" Int -> m (Maybe (Object m))
|
||||||
|
objectResolver (Arg objectId) = pure $ lookup objectId knownObjects
|
||||||
|
|
||||||
|
writerResolver :: Monad m => Arg "id" Int -> m (Maybe (Writer m))
|
||||||
|
writerResolver (Arg objectId) =
|
||||||
|
pure $ case lookup objectId knownObjects of
|
||||||
|
Just (ObjectWriter w) -> Just w
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
artistResolver :: Monad m => Arg "id" Int -> m (Maybe (Artist m))
|
||||||
|
artistResolver (Arg objectId) =
|
||||||
|
pure $ case lookup objectId knownObjects of
|
||||||
|
Just (ObjectArtist a) -> Just a
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
objectsResolver :: Monad m => Arg "ids" [Int] -> m [Maybe (Object m)]
|
||||||
|
objectsResolver (Arg objectIds) = pure [lookup objectId knownObjects | objectId <- objectIds]
|
||||||
|
|
||||||
|
articlesResolver :: Monad m => Arg "ids" [Int] -> m [Maybe (Article m)]
|
||||||
|
articlesResolver (Arg objectIds) =
|
||||||
|
pure $
|
||||||
|
objectIds <&> \objectId ->
|
||||||
|
case lookup objectId knownObjects of
|
||||||
|
Just (ObjectArticle a) -> Just a
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- RHS Postgres (for metadata only)
|
||||||
|
|
||||||
|
track :: Schema.Table
|
||||||
|
track =
|
||||||
|
Schema.Table
|
||||||
|
"track"
|
||||||
|
[ Schema.column "id" Schema.TInt,
|
||||||
|
Schema.column "title" Schema.TStr,
|
||||||
|
Schema.columnNull "album_id" Schema.TInt
|
||||||
|
]
|
||||||
|
["id"]
|
||||||
|
[]
|
||||||
|
[ [Schema.VInt 1, Schema.VStr "track1_album1", Schema.VInt 1],
|
||||||
|
[Schema.VInt 2, Schema.VStr "track2_album1", Schema.VInt 1],
|
||||||
|
[Schema.VInt 3, Schema.VStr "track3_album1", Schema.VInt 1],
|
||||||
|
[Schema.VInt 4, Schema.VStr "track1_album2", Schema.VInt 2],
|
||||||
|
[Schema.VInt 5, Schema.VStr "track2_album2", Schema.VInt 2],
|
||||||
|
[Schema.VInt 6, Schema.VStr "track1_album3", Schema.VInt 3],
|
||||||
|
[Schema.VInt 7, Schema.VStr "track2_album3", Schema.VInt 3],
|
||||||
|
[Schema.VInt 8, Schema.VStr "track_no_album", Schema.VNull]
|
||||||
|
]
|
||||||
|
|
||||||
|
rhsPostgresSetup :: State -> IO ()
|
||||||
|
rhsPostgresSetup state = do
|
||||||
|
let sourceName = "db"
|
||||||
|
sourceConfig = Postgres.defaultSourceConfiguration
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: pg_add_source
|
||||||
|
args:
|
||||||
|
name: *sourceName
|
||||||
|
configuration: *sourceConfig
|
||||||
|
|]
|
||||||
|
-- setup tables only
|
||||||
|
Postgres.createTable track
|
||||||
|
Postgres.insertTable track
|
||||||
|
Schema.trackTable Context.Postgres sourceName track state
|
||||||
|
|
||||||
|
rhsPostgresTeardown :: IO ()
|
||||||
|
rhsPostgresTeardown = Postgres.dropTable track
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- Tests
|
||||||
|
|
||||||
|
tests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
tests opts = do
|
||||||
|
-- tests metadata API
|
||||||
|
metadataAPITests
|
||||||
|
-- ensures setup is correct
|
||||||
|
noJoinsTests opts
|
||||||
|
simpleTests opts
|
||||||
|
-- joins on neither part of the union
|
||||||
|
joinArticleTests opts
|
||||||
|
-- joins on parts of the union
|
||||||
|
joinWriterTests opts
|
||||||
|
joinArtistTests opts
|
||||||
|
-- joins on deeply nested joins
|
||||||
|
deeplyNestedJoinTests opts
|
||||||
|
|
||||||
|
metadataAPITests :: SpecWith (State, Server)
|
||||||
|
metadataAPITests = describe "metadata API" do
|
||||||
|
it "adds a RS-RS relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical1
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [wIds]
|
||||||
|
remote_field:
|
||||||
|
articles:
|
||||||
|
arguments:
|
||||||
|
ids: $wIds
|
||||||
|
|]
|
||||||
|
it "adds a RS-RD relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical2
|
||||||
|
definition:
|
||||||
|
to_source:
|
||||||
|
source: db
|
||||||
|
table: {schema: hasura, name: track}
|
||||||
|
relationship_type: object
|
||||||
|
field_mapping:
|
||||||
|
id: id
|
||||||
|
|]
|
||||||
|
it "updates a RS-RS relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: update_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical1
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: remote
|
||||||
|
lhs_fields: [id]
|
||||||
|
remote_field:
|
||||||
|
artist:
|
||||||
|
arguments:
|
||||||
|
id: $id
|
||||||
|
|]
|
||||||
|
it "updates a RS-DB relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: update_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical2
|
||||||
|
definition:
|
||||||
|
to_source:
|
||||||
|
source: db
|
||||||
|
table: {schema: hasura, name: track}
|
||||||
|
relationship_type: array
|
||||||
|
field_mapping:
|
||||||
|
id: id
|
||||||
|
|]
|
||||||
|
it "deletes a RS-RS relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: delete_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical1
|
||||||
|
|]
|
||||||
|
it "deletes a RS-DB relationship" \(state, _) ->
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: delete_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: remote
|
||||||
|
type_name: Writer
|
||||||
|
name: nonsensical2
|
||||||
|
|]
|
||||||
|
|
||||||
|
-- | Ensure we don't insert `__hasura_internal_typename` when there are no
|
||||||
|
-- joins.
|
||||||
|
noJoinsTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
noJoinsTests opts = describe "simple joins" do
|
||||||
|
it "select objects without remote joins" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
obj101: object(id: 101) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
obj201: object(id: 201) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
obj301: object(id: 301) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
obj101:
|
||||||
|
__typename: Writer
|
||||||
|
obj201:
|
||||||
|
__typename: Artist
|
||||||
|
obj301:
|
||||||
|
__typename: Article
|
||||||
|
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
|
||||||
|
simpleTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
simpleTests opts = describe "simple joins" do
|
||||||
|
it "joins writer against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
writer(id: 101) {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
writer:
|
||||||
|
name: "Writer1"
|
||||||
|
articles:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article2
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
it "joins no writer against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
writer(id: 0) {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
writer: null
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
it "joins artist against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
artist(id: 201) {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
artist:
|
||||||
|
name: "Artist1"
|
||||||
|
articles:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article3
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
it "joins no artist against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
artist(id: 0) {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
artist: null
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
|
||||||
|
joinArticleTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
joinArticleTests opts = describe "join from article object" do
|
||||||
|
it "does not join" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
object(id: 301) {
|
||||||
|
... on Article {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
... on Artist {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Writer {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
object:
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Article
|
||||||
|
title: "Article1"
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
|
||||||
|
joinWriterTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
joinWriterTests opts = describe "join from writer object" do
|
||||||
|
it "joins against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
object(id: 101) {
|
||||||
|
... on Article {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
... on Artist {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Writer {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
object:
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Writer
|
||||||
|
name: "Writer1"
|
||||||
|
articles:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article2
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
|
||||||
|
joinArtistTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
joinArtistTests opts = describe "join from artist object" do
|
||||||
|
it "joins against articles" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
object(id: 201) {
|
||||||
|
... on Article {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
... on Artist {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Writer {
|
||||||
|
name
|
||||||
|
articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
object:
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Artist
|
||||||
|
name: "Artist1"
|
||||||
|
articles:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article3
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
|
||||||
|
deeplyNestedJoinTests :: Context.Options -> SpecWith (State, Server)
|
||||||
|
deeplyNestedJoinTests opts = describe "join from artist object" do
|
||||||
|
it "joins ambiguously nested articles depending on the full path" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
objects(ids: [101, 201]) {
|
||||||
|
... on Artist {
|
||||||
|
local_articles { # local join
|
||||||
|
title
|
||||||
|
foo: writer { # remote join
|
||||||
|
bar: wIds
|
||||||
|
baz: articles { # remote join
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Writer {
|
||||||
|
local_articles { # local join
|
||||||
|
title
|
||||||
|
foo: artist { # remote join
|
||||||
|
bar: aIds
|
||||||
|
baz: articles { # remote join
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
objects:
|
||||||
|
- local_articles:
|
||||||
|
- title: Article1
|
||||||
|
foo:
|
||||||
|
bar:
|
||||||
|
- 301
|
||||||
|
- 303
|
||||||
|
baz:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article3
|
||||||
|
- title: Article2
|
||||||
|
foo:
|
||||||
|
bar:
|
||||||
|
- 302
|
||||||
|
- 304
|
||||||
|
baz:
|
||||||
|
- title: Article2
|
||||||
|
- title: Article4
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Writer
|
||||||
|
- local_articles:
|
||||||
|
- title: Article1
|
||||||
|
foo:
|
||||||
|
bar:
|
||||||
|
- 301
|
||||||
|
- 302
|
||||||
|
baz:
|
||||||
|
- title: Article1
|
||||||
|
- title: Article2
|
||||||
|
- title: Article3
|
||||||
|
foo:
|
||||||
|
bar:
|
||||||
|
- 303
|
||||||
|
- 304
|
||||||
|
baz:
|
||||||
|
- title: Article3
|
||||||
|
- title: Article4
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Artist
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
||||||
|
it "joins nested articles at different depths" \(state, _) -> do
|
||||||
|
let query =
|
||||||
|
[graphql|
|
||||||
|
query {
|
||||||
|
objects(ids: [102, 202]) {
|
||||||
|
... on Artist {
|
||||||
|
bar: self {
|
||||||
|
baz: articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
... on Writer {
|
||||||
|
bar: articles {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|]
|
||||||
|
expectedResponse =
|
||||||
|
[yaml|
|
||||||
|
data:
|
||||||
|
objects:
|
||||||
|
- bar:
|
||||||
|
- title: Article3
|
||||||
|
- title: Article4
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Writer
|
||||||
|
- bar:
|
||||||
|
baz:
|
||||||
|
- title: Article2
|
||||||
|
- title: Article4
|
||||||
|
# to circumvent https://github.com/morpheusgraphql/morpheus-graphql/issues/687
|
||||||
|
__typename: Artist
|
||||||
|
|]
|
||||||
|
shouldReturnYaml
|
||||||
|
opts
|
||||||
|
(GraphqlEngine.postGraphql state query)
|
||||||
|
expectedResponse
|
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
-- | Tests for array remote relationships to databases. Remote relationships are
|
-- | Tests for array remote relationships to databases. Remote relationships are
|
||||||
@ -15,13 +16,24 @@ where
|
|||||||
import Control.Lens (findOf, has, only, (^?!))
|
import Control.Lens (findOf, has, only, (^?!))
|
||||||
import Data.Aeson (Value)
|
import Data.Aeson (Value)
|
||||||
import Data.Aeson.Lens (key, values, _String)
|
import Data.Aeson.Lens (key, values, _String)
|
||||||
import Data.Foldable (for_)
|
import Data.Char (isUpper, toLower)
|
||||||
|
import Data.Foldable (for_, traverse_)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (intercalate, sortBy)
|
||||||
|
import Data.List.Split (dropBlanks, keepDelimsL, split, whenElt)
|
||||||
import Data.Maybe qualified as Unsafe (fromJust)
|
import Data.Maybe qualified as Unsafe (fromJust)
|
||||||
|
import Data.Morpheus.Document (gqlDocument)
|
||||||
|
import Data.Morpheus.Types
|
||||||
|
import Data.Morpheus.Types qualified as Morpheus
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import Harness.Backend.Postgres qualified as Postgres
|
import Harness.Backend.Postgres qualified as Postgres
|
||||||
import Harness.GraphqlEngine qualified as GraphqlEngine
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
||||||
import Harness.Quoter.Graphql (graphql)
|
import Harness.Quoter.Graphql (graphql)
|
||||||
import Harness.Quoter.Yaml (shouldBeYaml, shouldReturnYaml, yaml)
|
import Harness.Quoter.Yaml (shouldBeYaml, shouldReturnYaml, yaml)
|
||||||
import Harness.State (Server, State)
|
import Harness.RemoteServer qualified as RemoteServer
|
||||||
|
import Harness.State (Server, State, stopServer)
|
||||||
import Harness.Test.Context (Context (..))
|
import Harness.Test.Context (Context (..))
|
||||||
import Harness.Test.Context qualified as Context
|
import Harness.Test.Context qualified as Context
|
||||||
import Harness.Test.Schema qualified as Schema
|
import Harness.Test.Schema qualified as Schema
|
||||||
@ -34,7 +46,7 @@ import Prelude
|
|||||||
spec :: SpecWith State
|
spec :: SpecWith State
|
||||||
spec = Context.runWithLocalState contexts tests
|
spec = Context.runWithLocalState contexts tests
|
||||||
where
|
where
|
||||||
lhsContexts = [lhsPostgres]
|
lhsContexts = [lhsPostgres, lhsRemoteServer]
|
||||||
rhsContexts = [rhsPostgres]
|
rhsContexts = [rhsPostgres]
|
||||||
contexts = combine <$> lhsContexts <*> rhsContexts
|
contexts = combine <$> lhsContexts <*> rhsContexts
|
||||||
|
|
||||||
@ -94,10 +106,15 @@ lhsPostgres tableName =
|
|||||||
customOptions = Nothing
|
customOptions = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
{-
|
lhsRemoteServer :: LHSContext
|
||||||
lhsRemoteServer :: Value -> Context
|
lhsRemoteServer tableName =
|
||||||
lhsRemoteServer tableName = Context "from RS" (lhsRemoteSetup tableName) lhsRemoteTeardown
|
Context
|
||||||
-}
|
{ name = Context.RemoteGraphQLServer,
|
||||||
|
mkLocalState = lhsRemoteServerMkLocalState,
|
||||||
|
setup = lhsRemoteServerSetup tableName,
|
||||||
|
teardown = lhsRemoteServerTeardown,
|
||||||
|
customOptions = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -231,10 +248,197 @@ args:
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
lhsPostgresTeardown :: (State, Maybe Server) -> IO ()
|
lhsPostgresTeardown :: (State, Maybe Server) -> IO ()
|
||||||
lhsPostgresTeardown (state, _) = do
|
lhsPostgresTeardown _ = Postgres.dropTable artist
|
||||||
let sourceName = "source"
|
|
||||||
Schema.untrackTable Context.Postgres sourceName artist state
|
--------------------------------------------------------------------------------
|
||||||
Postgres.dropTable artist
|
-- LHS Remote Server
|
||||||
|
|
||||||
|
-- | To circumvent Morpheus' default behaviour, which is to capitalize type
|
||||||
|
-- names and field names for Haskell records to be consistent with their
|
||||||
|
-- corresponding GraphQL equivalents, we define most of the schema manually with
|
||||||
|
-- the following options.
|
||||||
|
hasuraTypeOptions :: Morpheus.GQLTypeOptions
|
||||||
|
hasuraTypeOptions =
|
||||||
|
Morpheus.defaultTypeOptions
|
||||||
|
{ -- transformation to apply to constructors, for enums; we simply map to
|
||||||
|
-- lower case:
|
||||||
|
-- Asc -> asc
|
||||||
|
Morpheus.constructorTagModifier = map toLower,
|
||||||
|
-- transformation to apply to field names; we drop all characters up to and
|
||||||
|
-- including the first underscore:
|
||||||
|
-- hta_where -> where
|
||||||
|
Morpheus.fieldLabelModifier = tail . dropWhile (/= '_'),
|
||||||
|
-- transformation to apply to type names; we split the name on uppercase
|
||||||
|
-- letters, intercalate with underscore, and map everything to lowercase:
|
||||||
|
-- HasuraTrack -> hasura_track
|
||||||
|
Morpheus.typeNameModifier = \_ ->
|
||||||
|
map toLower
|
||||||
|
. intercalate "_"
|
||||||
|
. split (dropBlanks $ keepDelimsL $ whenElt isUpper)
|
||||||
|
}
|
||||||
|
|
||||||
|
data Query m = Query
|
||||||
|
{ hasura_artist :: HasuraArtistArgs -> m [HasuraArtist m]
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (Query m)
|
||||||
|
|
||||||
|
data HasuraArtistArgs = HasuraArtistArgs
|
||||||
|
{ aa_where :: Maybe HasuraArtistBoolExp,
|
||||||
|
aa_order_by :: Maybe [HasuraArtistOrderBy],
|
||||||
|
aa_limit :: Maybe Int
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraArtistArgs where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraArtist m = HasuraArtist
|
||||||
|
{ a_id :: m (Maybe Int),
|
||||||
|
a_name :: m (Maybe Text)
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (HasuraArtist m) where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraArtistOrderBy = HasuraArtistOrderBy
|
||||||
|
{ aob_id :: Maybe OrderType,
|
||||||
|
aob_name :: Maybe OrderType
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraArtistOrderBy where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraArtistBoolExp = HasuraArtistBoolExp
|
||||||
|
{ abe__and :: Maybe [HasuraArtistBoolExp],
|
||||||
|
abe__or :: Maybe [HasuraArtistBoolExp],
|
||||||
|
abe__not :: Maybe HasuraArtistBoolExp,
|
||||||
|
abe_id :: Maybe IntCompExp,
|
||||||
|
abe_name :: Maybe StringCompExp
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraArtistBoolExp where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data OrderType = Asc | Desc
|
||||||
|
deriving (Show, Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType OrderType where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
[gqlDocument|
|
||||||
|
|
||||||
|
input IntCompExp {
|
||||||
|
_eq: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input StringCompExp {
|
||||||
|
_eq: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerMkLocalState :: State -> IO (Maybe Server)
|
||||||
|
lhsRemoteServerMkLocalState _ = do
|
||||||
|
server <-
|
||||||
|
RemoteServer.run $
|
||||||
|
RemoteServer.generateQueryInterpreter (Query {hasura_artist})
|
||||||
|
pure $ Just server
|
||||||
|
where
|
||||||
|
-- Implements the @hasura_artist@ field of the @Query@ type.
|
||||||
|
hasura_artist (HasuraArtistArgs {..}) = do
|
||||||
|
let filterFunction = case aa_where of
|
||||||
|
Nothing -> const True
|
||||||
|
Just whereArg -> flip matchArtist whereArg
|
||||||
|
orderByFunction = case aa_order_by of
|
||||||
|
Nothing -> \_ _ -> EQ
|
||||||
|
Just orderByArg -> orderArtist orderByArg
|
||||||
|
limitFunction = case aa_limit of
|
||||||
|
Nothing -> Prelude.id
|
||||||
|
Just limitArg -> take limitArg
|
||||||
|
pure $
|
||||||
|
artists
|
||||||
|
& filter filterFunction
|
||||||
|
& sortBy orderByFunction
|
||||||
|
& limitFunction
|
||||||
|
& map mkArtist
|
||||||
|
-- Returns True iif the given artist matches the given boolean expression.
|
||||||
|
matchArtist artistInfo@(artistId, artistName) (HasuraArtistBoolExp {..}) =
|
||||||
|
and
|
||||||
|
[ maybe True (all (matchArtist artistInfo)) abe__and,
|
||||||
|
maybe True (any (matchArtist artistInfo)) abe__or,
|
||||||
|
maybe True (not . matchArtist artistInfo) abe__not,
|
||||||
|
maybe True (matchMaybeInt artistId) abe_id,
|
||||||
|
maybe True (matchString artistName) abe_name
|
||||||
|
]
|
||||||
|
matchString stringField StringCompExp {..} = Just stringField == _eq
|
||||||
|
matchMaybeInt maybeIntField IntCompExp {..} = maybeIntField == _eq
|
||||||
|
-- Returns an ordering between the two given artists.
|
||||||
|
orderArtist
|
||||||
|
orderByList
|
||||||
|
(artistId1, artistName1)
|
||||||
|
(artistId2, artistName2) =
|
||||||
|
flip foldMap orderByList \HasuraArtistOrderBy {..} ->
|
||||||
|
if
|
||||||
|
| Just idOrder <- aob_id ->
|
||||||
|
compareWithNullLast idOrder artistId1 artistId2
|
||||||
|
| Just nameOrder <- aob_name -> case nameOrder of
|
||||||
|
Asc -> compare artistName1 artistName2
|
||||||
|
Desc -> compare artistName2 artistName1
|
||||||
|
| otherwise ->
|
||||||
|
error "empty artist_order object"
|
||||||
|
compareWithNullLast Desc x1 x2 = compareWithNullLast Asc x2 x1
|
||||||
|
compareWithNullLast Asc Nothing Nothing = EQ
|
||||||
|
compareWithNullLast Asc (Just _) Nothing = LT
|
||||||
|
compareWithNullLast Asc Nothing (Just _) = GT
|
||||||
|
compareWithNullLast Asc (Just x1) (Just x2) = compare x1 x2
|
||||||
|
artists =
|
||||||
|
[ (Just 1, "artist1"),
|
||||||
|
(Just 2, "artist2"),
|
||||||
|
(Just 3, "artist_no_albums"),
|
||||||
|
(Nothing, "artist_no_id")
|
||||||
|
]
|
||||||
|
mkArtist (artistId, artistName) =
|
||||||
|
HasuraArtist
|
||||||
|
{ a_id = pure artistId,
|
||||||
|
a_name = pure $ Just artistName
|
||||||
|
}
|
||||||
|
|
||||||
|
lhsRemoteServerSetup :: Value -> (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerSetup tableName (state, maybeRemoteServer) = case maybeRemoteServer of
|
||||||
|
Nothing -> error "XToDBArrayRelationshipSpec: remote server local state did not succesfully create a server"
|
||||||
|
Just remoteServer -> do
|
||||||
|
let remoteSchemaEndpoint = GraphqlEngine.serverUrl remoteServer ++ "/graphql"
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
- type: add_remote_schema
|
||||||
|
args:
|
||||||
|
name: source
|
||||||
|
definition:
|
||||||
|
url: *remoteSchemaEndpoint
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: source
|
||||||
|
type_name: hasura_artist
|
||||||
|
name: albums
|
||||||
|
definition:
|
||||||
|
to_source:
|
||||||
|
source: target
|
||||||
|
table: *tableName
|
||||||
|
relationship_type: array
|
||||||
|
field_mapping:
|
||||||
|
id: artist_id
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerTeardown :: (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerTeardown (_, maybeServer) = traverse_ stopServer maybeServer
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- RHS Postgres
|
-- RHS Postgres
|
||||||
@ -294,16 +498,13 @@ args:
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
rhsPostgresTeardown :: (State, ()) -> IO ()
|
rhsPostgresTeardown :: (State, ()) -> IO ()
|
||||||
rhsPostgresTeardown (state, _) = do
|
rhsPostgresTeardown _ = Postgres.dropTable album
|
||||||
let sourceName = "target"
|
|
||||||
Schema.untrackTable Context.Postgres sourceName album state
|
|
||||||
Postgres.dropTable album
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Tests
|
-- Tests
|
||||||
|
|
||||||
tests :: Context.Options -> SpecWith (State, Maybe Server)
|
tests :: Context.Options -> SpecWith (State, Maybe Server)
|
||||||
tests opts = describe "array-relationship" $ do
|
tests opts = describe "array-relationship" do
|
||||||
schemaTests opts
|
schemaTests opts
|
||||||
executionTests opts
|
executionTests opts
|
||||||
permissionTests opts
|
permissionTests opts
|
||||||
@ -311,7 +512,7 @@ tests opts = describe "array-relationship" $ do
|
|||||||
schemaTests :: Context.Options -> SpecWith (State, Maybe Server)
|
schemaTests :: Context.Options -> SpecWith (State, Maybe Server)
|
||||||
schemaTests _opts =
|
schemaTests _opts =
|
||||||
-- we introspect the schema and validate it
|
-- we introspect the schema and validate it
|
||||||
it "graphql-schema" $ \(state, _) -> do
|
it "graphql-schema" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
fragment type_info on __Type {
|
fragment type_info on __Type {
|
||||||
@ -442,9 +643,9 @@ schemaTests _opts =
|
|||||||
|
|
||||||
-- | Basic queries using DB-to-DB joins
|
-- | Basic queries using DB-to-DB joins
|
||||||
executionTests :: Context.Options -> SpecWith (State, Maybe Server)
|
executionTests :: Context.Options -> SpecWith (State, Maybe Server)
|
||||||
executionTests opts = describe "execution" $ do
|
executionTests opts = describe "execution" do
|
||||||
-- fetches the relationship data
|
-- fetches the relationship data
|
||||||
it "related-data" $ \(state, _) -> do
|
it "related-data" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -471,7 +672,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- when there are no matching rows, the relationship response should be []
|
-- when there are no matching rows, the relationship response should be []
|
||||||
it "related-data-empty-array" $ \(state, _) -> do
|
it "related-data-empty-array" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -496,7 +697,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- when any of the join columns are null, the relationship should be null
|
-- when any of the join columns are null, the relationship should be null
|
||||||
it "related-data-null" $ \(state, _) -> do
|
it "related-data-null" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -521,7 +722,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- when the lhs response has both null and non-null values for join columns
|
-- when the lhs response has both null and non-null values for join columns
|
||||||
it "related-data-non-null-and-null" $ \(state, _) -> do
|
it "related-data-non-null-and-null" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -532,7 +733,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
{name: {_eq: "artist_no_id"}}
|
{name: {_eq: "artist_no_id"}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
order_by: {id: asc}
|
order_by: [{id: asc}]
|
||||||
) {
|
) {
|
||||||
name
|
name
|
||||||
albums {
|
albums {
|
||||||
@ -565,15 +766,15 @@ executionTests opts = describe "execution" $ do
|
|||||||
|
|
||||||
-- | tests that describe an array relationship's data in the presence of permisisons
|
-- | tests that describe an array relationship's data in the presence of permisisons
|
||||||
permissionTests :: Context.Options -> SpecWith (State, Maybe Server)
|
permissionTests :: Context.Options -> SpecWith (State, Maybe Server)
|
||||||
permissionTests opts = describe "permission" $ do
|
permissionTests opts = describe "permission" do
|
||||||
-- only the allowed rows on the target table are queryable
|
-- only the allowed rows on the target table are queryable
|
||||||
it "only-allowed-rows" $ \(state, _) -> do
|
it "only-allowed-rows" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role1"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role1"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
artist: hasura_artist(
|
artist: hasura_artist(
|
||||||
order_by: {id: asc}
|
order_by: [{id: asc}]
|
||||||
) {
|
) {
|
||||||
name
|
name
|
||||||
albums {
|
albums {
|
||||||
@ -605,7 +806,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
-- we use an introspection query to check column permissions:
|
-- we use an introspection query to check column permissions:
|
||||||
-- 1. the type 'hasura_album' has only 'artist_id' and 'title', the allowed columns
|
-- 1. the type 'hasura_album' has only 'artist_id' and 'title', the allowed columns
|
||||||
-- 2. the albums field in 'hasura_artist' type is of type 'hasura_album'
|
-- 2. the albums field in 'hasura_artist' type is of type 'hasura_album'
|
||||||
it "only-allowed-columns" $ \(state, _) -> do
|
it "only-allowed-columns" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role1"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role1"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -643,7 +844,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- _aggregate field should not be generated when 'allow_aggregations' isn't set to 'true'
|
-- _aggregate field should not be generated when 'allow_aggregations' isn't set to 'true'
|
||||||
it "aggregations-not-allowed" $ \(state, _) -> do
|
it "aggregations-not-allowed" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role1")]
|
let userHeaders = [("x-hasura-role", "role1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -670,7 +871,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- _aggregate field should only be allowed when 'allow_aggregations' is set to 'true'
|
-- _aggregate field should only be allowed when 'allow_aggregations' is set to 'true'
|
||||||
it "aggregations-allowed" $ \(state, _) -> do
|
it "aggregations-allowed" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2")]
|
let userHeaders = [("x-hasura-role", "role2")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -698,7 +899,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- permission limit should kick in when no query limit is specified
|
-- permission limit should kick in when no query limit is specified
|
||||||
it "no-query-limit" $ \(state, _) -> do
|
it "no-query-limit" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -727,7 +928,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- query limit should be applied when query limit <= permission limit
|
-- query limit should be applied when query limit <= permission limit
|
||||||
it "user-limit-less-than-permission-limit" $ \(state, _) -> do
|
it "user-limit-less-than-permission-limit" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -755,7 +956,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- permission limit should be applied when query limit > permission limit
|
-- permission limit should be applied when query limit > permission limit
|
||||||
it "user-limit-greater-than-permission-limit" $ \(state, _) -> do
|
it "user-limit-greater-than-permission-limit" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -784,7 +985,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- permission limit should only apply on 'nodes' but not on 'aggregate'
|
-- permission limit should only apply on 'nodes' but not on 'aggregate'
|
||||||
it "aggregations" $ \(state, _) -> do
|
it "aggregations" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
@ -821,7 +1022,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- query limit applies to both 'aggregate' and 'nodes'
|
-- query limit applies to both 'aggregate' and 'nodes'
|
||||||
it "aggregations-query-limit" $ \(state, _) -> do
|
it "aggregations-query-limit" \(state, _) -> do
|
||||||
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
let userHeaders = [("x-hasura-role", "role2"), ("x-hasura-artist-id", "1")]
|
||||||
query =
|
query =
|
||||||
[graphql|
|
[graphql|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
-- | Tests for object remote relationships to databases. Remote relationships
|
-- | Tests for object remote relationships to databases. Remote relationships
|
||||||
@ -14,11 +15,22 @@ module Test.RemoteRelationship.XToDBObjectRelationshipSpec
|
|||||||
where
|
where
|
||||||
|
|
||||||
import Data.Aeson (Value)
|
import Data.Aeson (Value)
|
||||||
|
import Data.Char (isUpper, toLower)
|
||||||
|
import Data.Foldable (traverse_)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (intercalate, sortBy)
|
||||||
|
import Data.List.Split (dropBlanks, keepDelimsL, split, whenElt)
|
||||||
|
import Data.Morpheus.Document (gqlDocument)
|
||||||
|
import Data.Morpheus.Types qualified as Morpheus
|
||||||
|
import Data.Text (Text)
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import GHC.Generics
|
||||||
import Harness.Backend.Postgres qualified as Postgres
|
import Harness.Backend.Postgres qualified as Postgres
|
||||||
import Harness.GraphqlEngine qualified as GraphqlEngine
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
||||||
import Harness.Quoter.Graphql (graphql)
|
import Harness.Quoter.Graphql (graphql)
|
||||||
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
||||||
import Harness.State (Server, State)
|
import Harness.RemoteServer qualified as RemoteServer
|
||||||
|
import Harness.State (Server, State, stopServer)
|
||||||
import Harness.Test.Context (Context (..))
|
import Harness.Test.Context (Context (..))
|
||||||
import Harness.Test.Context qualified as Context
|
import Harness.Test.Context qualified as Context
|
||||||
import Harness.Test.Schema qualified as Schema
|
import Harness.Test.Schema qualified as Schema
|
||||||
@ -31,7 +43,7 @@ import Prelude
|
|||||||
spec :: SpecWith State
|
spec :: SpecWith State
|
||||||
spec = Context.runWithLocalState contexts tests
|
spec = Context.runWithLocalState contexts tests
|
||||||
where
|
where
|
||||||
lhsContexts = [lhsPostgres]
|
lhsContexts = [lhsPostgres, lhsRemoteServer]
|
||||||
rhsContexts = [rhsPostgres]
|
rhsContexts = [rhsPostgres]
|
||||||
contexts = combine <$> lhsContexts <*> rhsContexts
|
contexts = combine <$> lhsContexts <*> rhsContexts
|
||||||
|
|
||||||
@ -91,10 +103,15 @@ lhsPostgres tableName =
|
|||||||
customOptions = Nothing
|
customOptions = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
{-
|
lhsRemoteServer :: LHSContext
|
||||||
lhsRemoteServer :: Value -> Context
|
lhsRemoteServer tableName =
|
||||||
lhsRemoteServer tableName = Context "from RS" (lhsRemoteSetup tableName) lhsRemoteTeardown
|
Context
|
||||||
-}
|
{ name = Context.RemoteGraphQLServer,
|
||||||
|
mkLocalState = lhsRemoteServerMkLocalState,
|
||||||
|
setup = lhsRemoteServerSetup tableName,
|
||||||
|
teardown = lhsRemoteServerTeardown,
|
||||||
|
customOptions = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
|
|
||||||
@ -232,10 +249,210 @@ args:
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
lhsPostgresTeardown :: (State, Maybe Server) -> IO ()
|
lhsPostgresTeardown :: (State, Maybe Server) -> IO ()
|
||||||
lhsPostgresTeardown (state, _) = do
|
lhsPostgresTeardown _ = Postgres.dropTable track
|
||||||
let sourceName = "source"
|
|
||||||
Schema.untrackTable Context.Postgres sourceName track state
|
--------------------------------------------------------------------------------
|
||||||
Postgres.dropTable track
|
-- LHS Remote Server
|
||||||
|
|
||||||
|
-- | To circumvent Morpheus' default behaviour, which is to capitalize type
|
||||||
|
-- names and field names for Haskell records to be consistent with their
|
||||||
|
-- corresponding GraphQL equivalents, we define most of the schema manually with
|
||||||
|
-- the following options.
|
||||||
|
hasuraTypeOptions :: Morpheus.GQLTypeOptions
|
||||||
|
hasuraTypeOptions =
|
||||||
|
Morpheus.defaultTypeOptions
|
||||||
|
{ -- transformation to apply to constructors, for enums; we simply map to
|
||||||
|
-- lower case:
|
||||||
|
-- Asc -> asc
|
||||||
|
Morpheus.constructorTagModifier = map toLower,
|
||||||
|
-- transformation to apply to field names; we drop all characters up to and
|
||||||
|
-- including the first underscore:
|
||||||
|
-- hta_where -> where
|
||||||
|
Morpheus.fieldLabelModifier = tail . dropWhile (/= '_'),
|
||||||
|
-- transformation to apply to type names; we split the name on uppercase
|
||||||
|
-- letters, intercalate with underscore, and map everything to lowercase:
|
||||||
|
-- HasuraTrack -> hasura_track
|
||||||
|
Morpheus.typeNameModifier = \_ ->
|
||||||
|
map toLower
|
||||||
|
. intercalate "_"
|
||||||
|
. split (dropBlanks $ keepDelimsL $ whenElt isUpper)
|
||||||
|
}
|
||||||
|
|
||||||
|
data Query m = Query
|
||||||
|
{ hasura_track :: HasuraTrackArgs -> m [HasuraTrack m]
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (Query m)
|
||||||
|
|
||||||
|
data HasuraTrackArgs = HasuraTrackArgs
|
||||||
|
{ ta_where :: Maybe HasuraTrackBoolExp,
|
||||||
|
ta_order_by :: Maybe [HasuraTrackOrderBy],
|
||||||
|
ta_limit :: Maybe Int
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraTrackArgs where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraTrack m = HasuraTrack
|
||||||
|
{ t_id :: m (Maybe Int),
|
||||||
|
t_title :: m (Maybe Text),
|
||||||
|
t_album_id :: m (Maybe Int)
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (HasuraTrack m) where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraTrackOrderBy = HasuraTrackOrderBy
|
||||||
|
{ tob_id :: Maybe OrderType,
|
||||||
|
tob_title :: Maybe OrderType,
|
||||||
|
tob_album_id :: Maybe OrderType
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraTrackOrderBy where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data HasuraTrackBoolExp = HasuraTrackBoolExp
|
||||||
|
{ tbe__and :: Maybe [HasuraTrackBoolExp],
|
||||||
|
tbe__or :: Maybe [HasuraTrackBoolExp],
|
||||||
|
tbe__not :: Maybe HasuraTrackBoolExp,
|
||||||
|
tbe_id :: Maybe IntCompExp,
|
||||||
|
tbe_title :: Maybe StringCompExp,
|
||||||
|
tbe_album_id :: Maybe IntCompExp
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType HasuraTrackBoolExp where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data OrderType = Asc | Desc
|
||||||
|
deriving (Show, Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType OrderType where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
[gqlDocument|
|
||||||
|
|
||||||
|
input IntCompExp {
|
||||||
|
_eq: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input StringCompExp {
|
||||||
|
_eq: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerMkLocalState :: State -> IO (Maybe Server)
|
||||||
|
lhsRemoteServerMkLocalState _ = do
|
||||||
|
server <-
|
||||||
|
RemoteServer.run $
|
||||||
|
RemoteServer.generateQueryInterpreter (Query {hasura_track})
|
||||||
|
pure $ Just server
|
||||||
|
where
|
||||||
|
-- Implements the @hasura_track@ field of the @Query@ type.
|
||||||
|
hasura_track (HasuraTrackArgs {..}) = do
|
||||||
|
let filterFunction = case ta_where of
|
||||||
|
Nothing -> const True
|
||||||
|
Just whereArg -> flip matchTrack whereArg
|
||||||
|
orderByFunction = case ta_order_by of
|
||||||
|
Nothing -> \_ _ -> EQ
|
||||||
|
Just orderByArg -> orderTrack orderByArg
|
||||||
|
limitFunction = case ta_limit of
|
||||||
|
Nothing -> Prelude.id
|
||||||
|
Just limitArg -> take limitArg
|
||||||
|
pure $
|
||||||
|
tracks
|
||||||
|
& filter filterFunction
|
||||||
|
& sortBy orderByFunction
|
||||||
|
& limitFunction
|
||||||
|
& map mkTrack
|
||||||
|
-- Returns True iif the given track matches the given boolean expression.
|
||||||
|
matchTrack trackInfo@(trackId, trackTitle, maybeAlbumId) (HasuraTrackBoolExp {..}) =
|
||||||
|
and
|
||||||
|
[ maybe True (all (matchTrack trackInfo)) tbe__and,
|
||||||
|
maybe True (any (matchTrack trackInfo)) tbe__or,
|
||||||
|
maybe True (not . matchTrack trackInfo) tbe__not,
|
||||||
|
maybe True (matchInt trackId) tbe_id,
|
||||||
|
maybe True (matchString trackTitle) tbe_title,
|
||||||
|
maybe True (matchMaybeInt maybeAlbumId) tbe_album_id
|
||||||
|
]
|
||||||
|
matchInt intField IntCompExp {..} = Just intField == _eq
|
||||||
|
matchString stringField StringCompExp {..} = Just stringField == _eq
|
||||||
|
matchMaybeInt maybeIntField IntCompExp {..} = maybeIntField == _eq
|
||||||
|
-- Returns an ordering between the two given tracks.
|
||||||
|
orderTrack
|
||||||
|
orderByList
|
||||||
|
(trackId1, trackTitle1, trackAlbumId1)
|
||||||
|
(trackId2, trackTitle2, trackAlbumId2) =
|
||||||
|
flip foldMap orderByList \HasuraTrackOrderBy {..} ->
|
||||||
|
if
|
||||||
|
| Just idOrder <- tob_id -> case idOrder of
|
||||||
|
Asc -> compare trackId1 trackId2
|
||||||
|
Desc -> compare trackId2 trackId1
|
||||||
|
| Just titleOrder <- tob_title -> case titleOrder of
|
||||||
|
Asc -> compare trackTitle1 trackTitle2
|
||||||
|
Desc -> compare trackTitle2 trackTitle1
|
||||||
|
| Just albumIdOrder <- tob_album_id ->
|
||||||
|
compareWithNullLast albumIdOrder trackAlbumId1 trackAlbumId2
|
||||||
|
| otherwise ->
|
||||||
|
error "empty track_order object"
|
||||||
|
compareWithNullLast Desc x1 x2 = compareWithNullLast Asc x2 x1
|
||||||
|
compareWithNullLast Asc Nothing Nothing = EQ
|
||||||
|
compareWithNullLast Asc (Just _) Nothing = LT
|
||||||
|
compareWithNullLast Asc Nothing (Just _) = GT
|
||||||
|
compareWithNullLast Asc (Just x1) (Just x2) = compare x1 x2
|
||||||
|
tracks =
|
||||||
|
[ (1, "track1_album1", Just 1),
|
||||||
|
(2, "track2_album1", Just 1),
|
||||||
|
(3, "track3_album1", Just 1),
|
||||||
|
(4, "track1_album2", Just 2),
|
||||||
|
(5, "track2_album2", Just 2),
|
||||||
|
(6, "track1_album3", Just 3),
|
||||||
|
(7, "track2_album3", Just 3),
|
||||||
|
(8, "track_no_album", Nothing)
|
||||||
|
]
|
||||||
|
mkTrack (trackId, title, albumId) =
|
||||||
|
HasuraTrack
|
||||||
|
{ t_id = pure $ Just trackId,
|
||||||
|
t_title = pure $ Just title,
|
||||||
|
t_album_id = pure albumId
|
||||||
|
}
|
||||||
|
|
||||||
|
lhsRemoteServerSetup :: Value -> (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerSetup tableName (state, maybeRemoteServer) = case maybeRemoteServer of
|
||||||
|
Nothing -> error "XToDBObjectRelationshipSpec: remote server local state did not succesfully create a server"
|
||||||
|
Just remoteServer -> do
|
||||||
|
let remoteSchemaEndpoint = GraphqlEngine.serverUrl remoteServer ++ "/graphql"
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
- type: add_remote_schema
|
||||||
|
args:
|
||||||
|
name: source
|
||||||
|
definition:
|
||||||
|
url: *remoteSchemaEndpoint
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: source
|
||||||
|
type_name: hasura_track
|
||||||
|
name: album
|
||||||
|
definition:
|
||||||
|
to_source:
|
||||||
|
source: target
|
||||||
|
table: *tableName
|
||||||
|
relationship_type: object
|
||||||
|
field_mapping:
|
||||||
|
album_id: id
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerTeardown :: (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerTeardown (_, maybeServer) = traverse_ stopServer maybeServer
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- RHS Postgres
|
-- RHS Postgres
|
||||||
@ -295,10 +512,7 @@ args:
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
rhsPostgresTeardown :: (State, ()) -> IO ()
|
rhsPostgresTeardown :: (State, ()) -> IO ()
|
||||||
rhsPostgresTeardown (state, _) = do
|
rhsPostgresTeardown _ = Postgres.dropTable album
|
||||||
let sourceName = "target"
|
|
||||||
Schema.untrackTable Context.Postgres sourceName album state
|
|
||||||
Postgres.dropTable album
|
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Tests
|
-- Tests
|
||||||
@ -375,7 +589,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
{title: {_eq: "track_no_album"}}
|
{title: {_eq: "track_no_album"}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
order_by: {id: asc}
|
order_by: [{id: asc}]
|
||||||
) {
|
) {
|
||||||
title
|
title
|
||||||
album {
|
album {
|
||||||
@ -411,7 +625,7 @@ permissionTests opts = describe "permission" $ do
|
|||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
track: hasura_track(
|
track: hasura_track(
|
||||||
order_by: {id: asc}
|
order_by: [{id: asc}]
|
||||||
) {
|
) {
|
||||||
title
|
title
|
||||||
album {
|
album {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
|
||||||
-- | Tests for remote relationships to remote schemas. Remote relationships are
|
-- | Tests for remote relationships to remote schemas. Remote relationships are
|
||||||
@ -12,15 +13,23 @@ module Test.RemoteRelationship.XToRemoteSchemaRelationshipSpec
|
|||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
|
import Data.Char (isUpper, toLower)
|
||||||
|
import Data.Foldable (traverse_)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (intercalate, sortBy)
|
||||||
|
import Data.List.Split (dropBlanks, keepDelimsL, split, whenElt)
|
||||||
import Data.Morpheus.Document (gqlDocument)
|
import Data.Morpheus.Document (gqlDocument)
|
||||||
import Data.Morpheus.Types (Arg (..))
|
import Data.Morpheus.Types
|
||||||
|
import Data.Morpheus.Types qualified as Morpheus
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
|
import Data.Typeable (Typeable)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
import Harness.Backend.Postgres qualified as Postgres
|
import Harness.Backend.Postgres qualified as Postgres
|
||||||
import Harness.GraphqlEngine qualified as GraphqlEngine
|
import Harness.GraphqlEngine qualified as GraphqlEngine
|
||||||
import Harness.Quoter.Graphql (graphql)
|
import Harness.Quoter.Graphql (graphql)
|
||||||
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
import Harness.Quoter.Yaml (shouldReturnYaml, yaml)
|
||||||
import Harness.RemoteServer qualified as RemoteServer
|
import Harness.RemoteServer qualified as RemoteServer
|
||||||
import Harness.State (Server, State)
|
import Harness.State (Server, State, stopServer)
|
||||||
import Harness.Test.Context (Context (..))
|
import Harness.Test.Context (Context (..))
|
||||||
import Harness.Test.Context qualified as Context
|
import Harness.Test.Context qualified as Context
|
||||||
import Harness.Test.Schema qualified as Schema
|
import Harness.Test.Schema qualified as Schema
|
||||||
@ -33,7 +42,7 @@ import Prelude
|
|||||||
spec :: SpecWith State
|
spec :: SpecWith State
|
||||||
spec = Context.runWithLocalState contexts tests
|
spec = Context.runWithLocalState contexts tests
|
||||||
where
|
where
|
||||||
contexts = map mkContext [lhsPostgres]
|
contexts = map mkContext [lhsPostgres, lhsRemoteServer]
|
||||||
lhsPostgres =
|
lhsPostgres =
|
||||||
Context
|
Context
|
||||||
{ name = Context.Backend Context.Postgres,
|
{ name = Context.Backend Context.Postgres,
|
||||||
@ -42,6 +51,14 @@ spec = Context.runWithLocalState contexts tests
|
|||||||
teardown = lhsPostgresTeardown,
|
teardown = lhsPostgresTeardown,
|
||||||
customOptions = Nothing
|
customOptions = Nothing
|
||||||
}
|
}
|
||||||
|
lhsRemoteServer =
|
||||||
|
Context
|
||||||
|
{ name = Context.RemoteGraphQLServer,
|
||||||
|
mkLocalState = lhsRemoteServerMkLocalState,
|
||||||
|
setup = lhsRemoteServerSetup,
|
||||||
|
teardown = lhsRemoteServerTeardown,
|
||||||
|
customOptions = Nothing
|
||||||
|
}
|
||||||
|
|
||||||
-- | Uses a given RHS context to create a combined context.
|
-- | Uses a given RHS context to create a combined context.
|
||||||
mkContext :: Context (Maybe Server) -> Context LocalTestState
|
mkContext :: Context (Maybe Server) -> Context LocalTestState
|
||||||
@ -154,6 +171,213 @@ lhsPostgresTeardown (state, _) = do
|
|||||||
Schema.untrackTable Context.Postgres sourceName track state
|
Schema.untrackTable Context.Postgres sourceName track state
|
||||||
Postgres.dropTable track
|
Postgres.dropTable track
|
||||||
|
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
-- LHS Remote Server
|
||||||
|
|
||||||
|
-- | To circumvent Morpheus' default behaviour, which is to capitalize type
|
||||||
|
-- names and field names for Haskell records to be consistent with their
|
||||||
|
-- corresponding GraphQL equivalents, we define most of the schema manually with
|
||||||
|
-- the following options.
|
||||||
|
hasuraTypeOptions :: Morpheus.GQLTypeOptions
|
||||||
|
hasuraTypeOptions =
|
||||||
|
Morpheus.defaultTypeOptions
|
||||||
|
{ -- transformation to apply to constructors, for enums; we simply map to
|
||||||
|
-- lower case:
|
||||||
|
-- Asc -> asc
|
||||||
|
Morpheus.constructorTagModifier = map toLower,
|
||||||
|
-- transformation to apply to field names; we drop all characters up to and
|
||||||
|
-- including the first underscore:
|
||||||
|
-- hta_where -> where
|
||||||
|
Morpheus.fieldLabelModifier = tail . dropWhile (/= '_'),
|
||||||
|
-- transformation to apply to type names; we remove the leading "LHS" we
|
||||||
|
-- use to differentiate those types from the RHS ones, split the name on
|
||||||
|
-- uppercase letters, intercalate with underscore, and map everything to
|
||||||
|
-- lowercase: LHSHasuraTrack -> hasura_track
|
||||||
|
Morpheus.typeNameModifier = \_ ->
|
||||||
|
map toLower
|
||||||
|
. intercalate "_"
|
||||||
|
. split (dropBlanks $ keepDelimsL $ whenElt isUpper)
|
||||||
|
. drop 3
|
||||||
|
}
|
||||||
|
|
||||||
|
data LHSQuery m = LHSQuery
|
||||||
|
{ q_hasura_track :: LHSHasuraTrackArgs -> m [LHSHasuraTrack m]
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (LHSQuery m) where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data LHSHasuraTrackArgs = LHSHasuraTrackArgs
|
||||||
|
{ ta_where :: Maybe LHSHasuraTrackBoolExp,
|
||||||
|
ta_order_by :: Maybe [LHSHasuraTrackOrderBy],
|
||||||
|
ta_limit :: Maybe Int
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType LHSHasuraTrackArgs where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data LHSHasuraTrack m = LHSHasuraTrack
|
||||||
|
{ t_id :: m (Maybe Int),
|
||||||
|
t_title :: m (Maybe Text),
|
||||||
|
t_album_id :: m (Maybe Int)
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Typeable m => Morpheus.GQLType (LHSHasuraTrack m) where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data LHSHasuraTrackOrderBy = LHSHasuraTrackOrderBy
|
||||||
|
{ tob_id :: Maybe LHSOrderType,
|
||||||
|
tob_title :: Maybe LHSOrderType,
|
||||||
|
tob_album_id :: Maybe LHSOrderType
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType LHSHasuraTrackOrderBy where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data LHSHasuraTrackBoolExp = LHSHasuraTrackBoolExp
|
||||||
|
{ tbe__and :: Maybe [LHSHasuraTrackBoolExp],
|
||||||
|
tbe__or :: Maybe [LHSHasuraTrackBoolExp],
|
||||||
|
tbe__not :: Maybe LHSHasuraTrackBoolExp,
|
||||||
|
tbe_id :: Maybe IntCompExp,
|
||||||
|
tbe_title :: Maybe StringCompExp,
|
||||||
|
tbe_album_id :: Maybe IntCompExp
|
||||||
|
}
|
||||||
|
deriving (Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType LHSHasuraTrackBoolExp where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
data LHSOrderType = Asc | Desc
|
||||||
|
deriving (Show, Generic)
|
||||||
|
|
||||||
|
instance Morpheus.GQLType LHSOrderType where
|
||||||
|
typeOptions _ _ = hasuraTypeOptions
|
||||||
|
|
||||||
|
[gqlDocument|
|
||||||
|
|
||||||
|
input IntCompExp {
|
||||||
|
_eq: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
input StringCompExp {
|
||||||
|
_eq: String
|
||||||
|
}
|
||||||
|
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerMkLocalState :: State -> IO (Maybe Server)
|
||||||
|
lhsRemoteServerMkLocalState _ = do
|
||||||
|
server <-
|
||||||
|
RemoteServer.run $
|
||||||
|
RemoteServer.generateQueryInterpreter (LHSQuery {q_hasura_track = hasura_track})
|
||||||
|
pure $ Just server
|
||||||
|
where
|
||||||
|
-- Implements the @hasura_track@ field of the @Query@ type.
|
||||||
|
hasura_track (LHSHasuraTrackArgs {..}) = do
|
||||||
|
let filterFunction = case ta_where of
|
||||||
|
Nothing -> const True
|
||||||
|
Just whereArg -> flip matchTrack whereArg
|
||||||
|
orderByFunction = case ta_order_by of
|
||||||
|
Nothing -> \_ _ -> EQ
|
||||||
|
Just orderByArg -> orderTrack orderByArg
|
||||||
|
limitFunction = case ta_limit of
|
||||||
|
Nothing -> Prelude.id
|
||||||
|
Just limitArg -> take limitArg
|
||||||
|
pure $
|
||||||
|
tracks
|
||||||
|
& filter filterFunction
|
||||||
|
& sortBy orderByFunction
|
||||||
|
& limitFunction
|
||||||
|
& map mkTrack
|
||||||
|
-- Returns True iif the given track matches the given boolean expression.
|
||||||
|
matchTrack trackInfo@(trackId, trackTitle, maybeAlbumId) (LHSHasuraTrackBoolExp {..}) =
|
||||||
|
and
|
||||||
|
[ maybe True (all (matchTrack trackInfo)) tbe__and,
|
||||||
|
maybe True (any (matchTrack trackInfo)) tbe__or,
|
||||||
|
maybe True (not . matchTrack trackInfo) tbe__not,
|
||||||
|
maybe True (matchInt trackId) tbe_id,
|
||||||
|
maybe True (matchString trackTitle) tbe_title,
|
||||||
|
maybe True (matchMaybeInt maybeAlbumId) tbe_album_id
|
||||||
|
]
|
||||||
|
matchInt intField IntCompExp {..} = Just intField == _eq
|
||||||
|
matchString stringField StringCompExp {..} = Just stringField == _eq
|
||||||
|
matchMaybeInt maybeIntField IntCompExp {..} = maybeIntField == _eq
|
||||||
|
-- Returns an ordering between the two given tracks.
|
||||||
|
orderTrack
|
||||||
|
orderByList
|
||||||
|
(trackId1, trackTitle1, trackAlbumId1)
|
||||||
|
(trackId2, trackTitle2, trackAlbumId2) =
|
||||||
|
flip foldMap orderByList \LHSHasuraTrackOrderBy {..} ->
|
||||||
|
if
|
||||||
|
| Just idOrder <- tob_id -> case idOrder of
|
||||||
|
Asc -> compare trackId1 trackId2
|
||||||
|
Desc -> compare trackId2 trackId1
|
||||||
|
| Just titleOrder <- tob_title -> case titleOrder of
|
||||||
|
Asc -> compare trackTitle1 trackTitle2
|
||||||
|
Desc -> compare trackTitle2 trackTitle1
|
||||||
|
| Just albumIdOrder <- tob_album_id ->
|
||||||
|
compareWithNullLast albumIdOrder trackAlbumId1 trackAlbumId2
|
||||||
|
| otherwise ->
|
||||||
|
error "empty track_order object"
|
||||||
|
compareWithNullLast Desc x1 x2 = compareWithNullLast Asc x2 x1
|
||||||
|
compareWithNullLast Asc Nothing Nothing = EQ
|
||||||
|
compareWithNullLast Asc (Just _) Nothing = LT
|
||||||
|
compareWithNullLast Asc Nothing (Just _) = GT
|
||||||
|
compareWithNullLast Asc (Just x1) (Just x2) = compare x1 x2
|
||||||
|
tracks =
|
||||||
|
[ (1, "track1_album1", Just 1),
|
||||||
|
(2, "track2_album1", Just 1),
|
||||||
|
(3, "track3_album1", Just 1),
|
||||||
|
(4, "track1_album2", Just 2),
|
||||||
|
(5, "track2_album2", Just 2),
|
||||||
|
(6, "track1_album3", Just 3),
|
||||||
|
(7, "track2_album3", Just 3),
|
||||||
|
(8, "track_no_album", Nothing)
|
||||||
|
]
|
||||||
|
mkTrack (trackId, title, albumId) =
|
||||||
|
LHSHasuraTrack
|
||||||
|
{ t_id = pure $ Just trackId,
|
||||||
|
t_title = pure $ Just title,
|
||||||
|
t_album_id = pure albumId
|
||||||
|
}
|
||||||
|
|
||||||
|
lhsRemoteServerSetup :: (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerSetup (state, maybeRemoteServer) = case maybeRemoteServer of
|
||||||
|
Nothing -> error "XToDBObjectRelationshipSpec: remote server local state did not succesfully create a server"
|
||||||
|
Just remoteServer -> do
|
||||||
|
let remoteSchemaEndpoint = GraphqlEngine.serverUrl remoteServer ++ "/graphql"
|
||||||
|
GraphqlEngine.postMetadata_
|
||||||
|
state
|
||||||
|
[yaml|
|
||||||
|
type: bulk
|
||||||
|
args:
|
||||||
|
- type: add_remote_schema
|
||||||
|
args:
|
||||||
|
name: source
|
||||||
|
definition:
|
||||||
|
url: *remoteSchemaEndpoint
|
||||||
|
- type: create_remote_schema_remote_relationship
|
||||||
|
args:
|
||||||
|
remote_schema: source
|
||||||
|
type_name: hasura_track
|
||||||
|
name: album
|
||||||
|
definition:
|
||||||
|
to_remote_schema:
|
||||||
|
remote_schema: target
|
||||||
|
lhs_fields: [album_id]
|
||||||
|
remote_field:
|
||||||
|
album:
|
||||||
|
arguments:
|
||||||
|
album_id: $album_id
|
||||||
|
|]
|
||||||
|
|
||||||
|
lhsRemoteServerTeardown :: (State, Maybe Server) -> IO ()
|
||||||
|
lhsRemoteServerTeardown (_, maybeServer) = traverse_ stopServer maybeServer
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- RHS Remote Server
|
-- RHS Remote Server
|
||||||
|
|
||||||
@ -203,21 +427,21 @@ args:
|
|||||||
|]
|
|]
|
||||||
|
|
||||||
rhsRemoteSchemaTeardown :: (State, Server) -> IO ()
|
rhsRemoteSchemaTeardown :: (State, Server) -> IO ()
|
||||||
rhsRemoteSchemaTeardown (_, server) = GraphqlEngine.stopServer server
|
rhsRemoteSchemaTeardown (_, server) = stopServer server
|
||||||
|
|
||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Tests
|
-- Tests
|
||||||
|
|
||||||
tests :: Context.Options -> SpecWith (State, LocalTestState)
|
tests :: Context.Options -> SpecWith (State, LocalTestState)
|
||||||
tests opts = describe "remote-schema-relationship" $ do
|
tests opts = describe "remote-schema-relationship" do
|
||||||
schemaTests opts
|
schemaTests opts
|
||||||
executionTests opts
|
executionTests opts
|
||||||
|
|
||||||
-- | Basic queries using *-to-DB joins
|
-- | Basic queries using *-to-DB joins
|
||||||
executionTests :: Context.Options -> SpecWith (State, LocalTestState)
|
executionTests :: Context.Options -> SpecWith (State, LocalTestState)
|
||||||
executionTests opts = describe "execution" $ do
|
executionTests opts = describe "execution" do
|
||||||
-- fetches the relationship data
|
-- fetches the relationship data
|
||||||
it "related-data" $ \(state, _) -> do
|
it "related-data" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -243,7 +467,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- when any of the join columns are null, the relationship should be null
|
-- when any of the join columns are null, the relationship should be null
|
||||||
it "related-data-null" $ \(state, _) -> do
|
it "related-data-null" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -268,7 +492,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
expectedResponse
|
expectedResponse
|
||||||
|
|
||||||
-- when the lhs response has both null and non-null values for join columns
|
-- when the lhs response has both null and non-null values for join columns
|
||||||
it "related-data-non-null-and-null" $ \(state, _) -> do
|
it "related-data-non-null-and-null" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
@ -279,7 +503,7 @@ executionTests opts = describe "execution" $ do
|
|||||||
{title: {_eq: "track_no_album"}}
|
{title: {_eq: "track_no_album"}}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
order_by: {id: asc}
|
order_by: [{id: asc}]
|
||||||
) {
|
) {
|
||||||
title
|
title
|
||||||
album {
|
album {
|
||||||
@ -309,7 +533,7 @@ schemaTests opts =
|
|||||||
-- 1. a field 'album' is added to the track table
|
-- 1. a field 'album' is added to the track table
|
||||||
-- 1. track's where clause does not have 'album' field
|
-- 1. track's where clause does not have 'album' field
|
||||||
-- 2. track's order_by clause does nat have 'album' field
|
-- 2. track's order_by clause does nat have 'album' field
|
||||||
it "graphql-schema" $ \(state, _) -> do
|
it "graphql-schema" \(state, _) -> do
|
||||||
let query =
|
let query =
|
||||||
[graphql|
|
[graphql|
|
||||||
query {
|
query {
|
||||||
|
@ -197,7 +197,7 @@ class TestRemoteSchemaBasic:
|
|||||||
"""add 2 remote schemas with same node or types"""
|
"""add 2 remote schemas with same node or types"""
|
||||||
q = mk_add_remote_q('simple 2', 'http://localhost:5000/hello-graphql')
|
q = mk_add_remote_q('simple 2', 'http://localhost:5000/hello-graphql')
|
||||||
st_code, resp = hge_ctx.v1q(q)
|
st_code, resp = hge_ctx.v1q(q)
|
||||||
assert st_code == 400
|
assert st_code == 400, resp
|
||||||
assert resp['code'] == 'unexpected'
|
assert resp['code'] == 'unexpected'
|
||||||
|
|
||||||
@pytest.mark.allow_server_upgrade_test
|
@pytest.mark.allow_server_upgrade_test
|
||||||
@ -346,7 +346,7 @@ class TestAddRemoteSchemaTbls:
|
|||||||
"""add remote schema which conflicts with hasura tables"""
|
"""add remote schema which conflicts with hasura tables"""
|
||||||
q = mk_add_remote_q('simple2', 'http://localhost:5000/hello-graphql')
|
q = mk_add_remote_q('simple2', 'http://localhost:5000/hello-graphql')
|
||||||
st_code, resp = hge_ctx.v1q(q)
|
st_code, resp = hge_ctx.v1q(q)
|
||||||
assert st_code == 400
|
assert st_code == 400, resp
|
||||||
assert resp['code'] == 'invalid-configuration'
|
assert resp['code'] == 'invalid-configuration'
|
||||||
|
|
||||||
@pytest.mark.allow_server_upgrade_test
|
@pytest.mark.allow_server_upgrade_test
|
||||||
|
Loading…
Reference in New Issue
Block a user