1
Fork 0
mirror of https://github.com/Airsequel/AirGQL.git synced 2025-07-27 02:18:43 +03:00
airgql/tests/Tests/IntrospectionSpec.hs
Adrian Sieber be818d626c Merge pull request from feramhq/hasura-comparison-docs-page
Support more of Hasura's syntax
2025-05-04 21:50:21 +00:00

838 lines
25 KiB
Haskell

{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# OPTIONS_GHC -Wno-missing-local-signatures #-}
module Tests.IntrospectionSpec (main) where
import Protolude (
Either (Right),
Maybe (Nothing),
Monoid (mempty),
Text,
readFile,
void,
($),
)
import Data.Aeson qualified as Ae
import Database.SQLite.Simple qualified as SS
import Database.SQLite.Simple.QQ (sql)
import Language.GraphQL.Class (gql)
import Language.GraphQL.JSON (graphql)
import System.Directory (makeAbsolute)
import System.FilePath ((</>))
import Test.Hspec (Spec, describe, it, shouldBe)
import AirGQL.GraphQL (getDerivedSchema)
import AirGQL.Lib (getEnrichedTables, insertOnly, writeOnly)
import AirGQL.Raw (raw)
import AirGQL.Types.SchemaConf (SchemaConf (accessMode), defaultSchemaConf)
import AirGQL.Utils (withRetryConn)
import Tests.Utils (dbPath, fixtureDbId, rmSpaces, testRoot, unorderedShouldBe, withTestDbConn)
main :: Spec
main = void $ do
describe "Query" $ do
it "supports a minimal introspection query" $ do
let
introspectionQuery :: Text
introspectionQuery =
[gql|
query IntrospectionQuery {
__schema {
queryType { name }
}
}
|]
expected =
rmSpaces
[raw|
{
"data": {
"__schema": {
"queryType": {
"name": "Query"
}
}
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
Ae.encode result `shouldBe` expected
it "supports an optional filter argument" $ do
let
introspectionQuery :: Text
introspectionQuery =
[gql|
query IntrospectionQuery {
__schema {
queryType {
name
fields {
name
args {
name
type {
name
}
}
}
}
}
}
|]
expected =
rmSpaces
[raw|{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"fields": [
{
"name": "users",
"args": [
{ "name": "where",
"type": { "name": "users_filter" }
},
{ "name": "order_by",
"type": { "name": null }
},
{ "name": "limit",
"type": { "name": "Int" }
},
{ "name": "offset",
"type": { "name": "Int" }
}
]
},
{
"name": "songs",
"args": [
{ "name": "where",
"type": { "name": "songs_filter" }
},
{ "name": "order_by",
"type": { "name": null }
},
{ "name": "limit",
"type": { "name": "Int" }
},
{ "name": "offset",
"type": { "name": "Int" }
}
]
},
{
"name": "users_by_pk",
"args": [
{ "name": "email",
"type": { "name": null }
}
]
},
{
"name": "songs_by_pk",
"args": [
{ "name": "rowid",
"type": { "name": null }
}
]
}
]
}
}
}
}|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
Ae.encode result `shouldBe` expected
it "doesn't allow writeonly tokens to read data" $ do
let
introspectionQuery :: Text
introspectionQuery =
[gql|
query IntrospectionQuery {
__schema {
queryType { name }
}
}
|]
expected =
rmSpaces
[raw|
{
"data": null,
"errors": [{
"locations": [{ "column":3, "line":2 }],
"message": "user error (Cannot read field using the provided token)",
"path": ["__schema"]
}]
}
|]
schema <- withRetryConn dbPath $ \conn -> do
Right tables <- getEnrichedTables conn
getDerivedSchema
defaultSchemaConf{accessMode = writeOnly}
conn
fixtureDbId
tables
Right response <-
graphql schema Nothing mempty introspectionQuery
Ae.encode response `shouldBe` expected
describe "Mutation" $ do
it "supports introspection queries" $ do
let
introspectionQuery :: Text
introspectionQuery =
[gql|
query IntrospectionQuery {
__schema {
mutationType {
name
fields {
name
args {
name
}
}
}
}
}
|]
expected =
[raw|
{
"data": {
"__schema": {
"mutationType": {
"name": "Mutation",
"fields": [
{
"name": "insert_users",
"args": [ { "name": "objects" }, { "name": "on_conflict" } ]
},
{
"name": "update_users",
"args": [
{ "name": "where" },
{ "name": "_set" }
]
},
{
"name": "update_users_by_pk",
"args": [
{ "name": "email" },
{ "name": "_set" }
]
},
{
"name": "delete_users",
"args": [ { "name": "where" } ]
},
{
"name": "delete_users_by_pk",
"args": [ { "name": "email" } ]
},
{
"name": "insert_songs",
"args": [ { "name": "objects" }, { "name": "on_conflict" } ]
},
{
"name": "update_songs",
"args": [
{ "name": "where" },
{ "name": "_set" }
]
},
{
"name": "update_songs_by_pk",
"args": [
{ "name": "rowid" },
{ "name": "_set" }
]
},
{
"name": "delete_songs",
"args": [ { "name": "where" } ]
},
{
"name": "delete_songs_by_pk",
"args": [ { "name": "rowid" } ]
}
]
}
}
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
result `unorderedShouldBe` expected
it "supports __typename on root query" $ do
let
introspectionQuery =
[gql|
query TypeName {
__typename
}
|]
expected =
rmSpaces
[raw|
{
"data": {
"__typename" : "Query"
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
Ae.encode result `shouldBe` expected
it "returns fields for {query,mutation,subscription}Type" $ do
let
introspectionQuery =
[gql|
query {
__schema {
queryType { fields { name } }
mutationType { fields { name } }
subscriptionType { fields { name } }
}
}
|]
expected =
[raw|
{
"data": {
"__schema": {
"queryType": {
"fields": [
{ "name": "users" },
{ "name": "songs" },
{ "name": "users_by_pk" },
{ "name": "songs_by_pk" }
]
},
"subscriptionType": null,
"mutationType": {
"fields": [
{ "name": "insert_users" },
{ "name": "update_users" },
{ "name": "delete_users" },
{ "name": "insert_songs" },
{ "name": "update_songs" },
{ "name": "delete_songs" },
{ "name": "update_users_by_pk" },
{ "name": "delete_users_by_pk" },
{ "name": "update_songs_by_pk" },
{ "name": "delete_songs_by_pk" }
]
}
}
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
result `unorderedShouldBe` expected
it "supports __typename fields" $ do
let
introspectionQuery =
[gql|
query UsersTypeName {
users {
__typename
}
}
|]
expected =
rmSpaces
[raw|
{
"data": {
"users": [
{ "__typename" : "users_row" }
]
}
}
|]
conn <- SS.open dbPath
SS.execute_
conn
[sql|
insert into users (name, email, created_utc)
values ('John', 'john@example.com', '2022-01-01T00:00Z')
|]
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
Ae.encode result `shouldBe` expected
it "returns types" $ do
let
introspectionQuery =
[gql|
query {
__schema {
types {
kind
name
}
}
}
|]
expected =
[raw|
{
"data": {
"__schema": {
"types": [
{ "kind": "OBJECT", "name": "users_row" },
{ "kind": "OBJECT", "name": "users_mutation_response" },
{ "kind": "OBJECT", "name": "users_mutation_by_pk_response" },
{ "kind": "INPUT_OBJECT", "name": "users_insert_input" },
{ "kind": "ENUM", "name": "users_column" },
{ "kind": "INPUT_OBJECT", "name": "users_upsert_on_conflict" },
{ "kind": "INPUT_OBJECT", "name": "users_set_input" },
{ "kind": "INPUT_OBJECT", "name": "users_filter" },
{ "kind": "INPUT_OBJECT", "name": "users_order_by" },
{ "kind": "OBJECT", "name": "songs_row" },
{ "kind": "OBJECT", "name": "songs_mutation_response" },
{ "kind": "OBJECT", "name": "songs_mutation_by_pk_response" },
{ "kind": "INPUT_OBJECT", "name": "songs_insert_input" },
{ "kind": "ENUM", "name": "songs_column" },
{ "kind": "INPUT_OBJECT", "name": "songs_upsert_on_conflict" },
{ "kind": "INPUT_OBJECT", "name": "songs_set_input" },
{ "kind": "INPUT_OBJECT", "name": "songs_filter" },
{ "kind": "INPUT_OBJECT", "name": "songs_order_by" },
{ "kind": "INPUT_OBJECT", "name": "IntComparison" },
{ "kind": "INPUT_OBJECT", "name": "FloatComparison" },
{ "kind": "INPUT_OBJECT", "name": "StringComparison" },
{ "kind": "ENUM", "name": "OrderingTerm" },
{ "kind": "OBJECT", "name": "Query" },
{ "kind": "OBJECT", "name": "Mutation" },
{ "kind": "SCALAR", "name": "Boolean" },
{ "kind": "SCALAR", "name": "Int" },
{ "kind": "SCALAR", "name": "Float" },
{ "kind": "SCALAR", "name": "String" },
{ "kind": "SCALAR", "name": "ID" },
{ "kind": "OBJECT", "name": "__Schema" },
{ "kind": "OBJECT", "name": "__Type" },
{ "kind": "ENUM", "name": "__TypeKind" },
{ "kind": "OBJECT", "name": "__Field" },
{ "kind": "OBJECT", "name": "__InputValue" },
{ "kind": "OBJECT", "name": "__EnumValue" },
{ "kind": "OBJECT", "name": "__Directive" },
{ "kind": "ENUM", "name": "__DirectiveLocation" }
]
}
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
result `unorderedShouldBe` expected
it "returns directives on __schema" $ do
let
introspectionQuery :: Text =
[gql|
query UsersTypeName {
__schema {
directives {
name
description
locations
args {
name
description
defaultValue
type { ...TypeRef }
}
}
}
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
|]
expected =
rmSpaces
[raw|
{
"data": {
"__schema": {
"directives": [
{
"name": "skip",
"args": [
{
"name": "if",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind":"SCALAR",
"name":"Boolean",
"ofType":null
}
},
"description": "Skipped when true."
}
],
"locations": [
"INLINE_FRAGMENT",
"FRAGMENT_SPREAD",
"FIELD"
],
"description": "Directs the executor to skip this field or fragment when the `if` argument is true."
},
{
"name": "include",
"args": [
{
"name": "if",
"defaultValue": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind":"SCALAR",
"name":"Boolean",
"ofType":null
}
},
"description": "Included when true."
}
],
"locations": [
"INLINE_FRAGMENT",
"FRAGMENT_SPREAD",
"FIELD"
],
"description":
"Directs the executor to include this field or fragment only when the `if` argument is true."
}
]
}
}
}
|]
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
Ae.encode result `shouldBe` expected
it "supports a full introspection query" $ do
gqlFile <- makeAbsolute $ testRoot </> "introspection_query.gql"
introspectionQuery <- readFile gqlFile
jsonFile <- makeAbsolute $ testRoot </> "introspection_result.json"
expected <- readFile jsonFile
conn <- SS.open dbPath
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
-- Uncomment to write the new file to disk (for easier diffing)
-- P.writeFile (testRoot </> "new_introspection_result.json") $
-- P.decodeUtf8 $
-- BL.toStrict $
-- Ae.encode result
result `unorderedShouldBe` expected
it "doesn't allow writeonly tokens to return data" $ do
let dbName = "no-writeonly-return.db"
withTestDbConn dbName $ \conn -> do
SS.execute_
conn
[sql|
CREATE TABLE items (
id INTEGER PRIMARY KEY
)
|]
let
query :: Text
query =
[gql|
mutation items {
update_items(filter: { id: { eq: 0 }}, set: { id: 0 }) {
returning { id }
}
}
|]
expected =
rmSpaces
[raw|
{
"data": null,
"errors": [{
"locations": [{ "column":5, "line":3 }],
"message": "Cannot query field \"returning\" on type \"items_mutation_response\"."
}]
}
|]
Right tables <- getEnrichedTables conn
schema <-
getDerivedSchema
defaultSchemaConf{accessMode = writeOnly}
conn
fixtureDbId
tables
Right response <-
graphql schema Nothing mempty query
Ae.encode response `shouldBe` expected
it "doesn't allow insertonly tokens to update data" $ do
let dbName = "no-insertonly-return.db"
withTestDbConn dbName $ \conn -> do
SS.execute_
conn
[sql|
CREATE TABLE items (
id INTEGER PRIMARY KEY
)
|]
let
query :: Text
query =
[gql|
mutation items {
update_items_by_pk(id: 0, set: { id: 0 }) {
affected_rows
}
}
|]
expected =
rmSpaces
[raw|
{
"data": null,
"errors": [{
"locations": [{ "column":3, "line":2 }],
"message": "Cannot query field \"update_items_by_pk\" on type \"Mutation\"."
}]
}
|]
Right tables <- getEnrichedTables conn
schema <-
getDerivedSchema
defaultSchemaConf{accessMode = insertOnly}
conn
fixtureDbId
tables
Right response <-
graphql schema Nothing mempty query
Ae.encode response `shouldBe` expected
describe "Naming conflicts" $ do
it "appends _ at the end of queries to avoid conflicts with table names" $ do
let dbName = "by-pk-table-names.db"
withTestDbConn dbName $ \conn -> do
SS.execute_ conn [sql| CREATE TABLE foo (id INT) |]
SS.execute_ conn [sql| CREATE TABLE foo_by_pk (id INT) |]
SS.execute_ conn [sql| CREATE TABLE foo_by_pk_ (id INT) |]
let
introspectionQuery =
[gql|
query {
__schema {
queryType { fields { name } }
}
}
|]
expected =
[raw|
{
"data": {
"__schema": {
"queryType": {
"fields": [
{ "name": "foo" },
{ "name": "foo_by_pk" },
{ "name": "foo_by_pk_" },
{ "name": "foo_by_pk__" },
{ "name": "foo_by_pk_by_pk" },
{ "name": "foo_by_pk__by_pk" }
]
}
}
}
}
|]
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
result `unorderedShouldBe` expected
it "appends _ at the end of argument names to avoid conflicts" $ do
let dbName = "by-pk-arg-names.db"
withTestDbConn dbName $ \conn -> do
SS.execute_ conn [sql| CREATE TABLE foo ("_set" INT PRIMARY KEY) |]
let
introspectionQuery =
[gql|
query {
__schema {
mutationType { fields { name, args { name } } }
}
}
|]
expected =
[raw|
{
"data": {
"__schema": {
"mutationType": {
"fields": [
{
"name": "insert_foo",
"args": [
{ "name": "objects" },
{ "name": "on_conflict" }
]
},
{
"name": "update_foo",
"args": [
{ "name": "_set" },
{ "name": "where" }
]
},
{
"name": "update_foo_by_pk",
"args": [
{ "name": "_set" },
{ "name": "_set_" }
]
},
{
"name": "delete_foo",
"args": [{ "name": "where" }]
},
{
"name": "delete_foo_by_pk",
"args": [{ "name": "_set" }]
}
]
}
}
}
}
|]
Right tables <- getEnrichedTables conn
schema <- getDerivedSchema defaultSchemaConf conn fixtureDbId tables
Right result <- graphql schema Nothing mempty introspectionQuery
result `unorderedShouldBe` expected