graphql-engine/server/tests-hspec/Test/RemoteRelationship/FromRemoteSchemaSpec.hs
Antoine Leblanc ccea1da1d5 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
2022-03-17 20:54:57 +00:00

748 lines
20 KiB
Haskell

{-# 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