graphql-engine/server/tests-py/test_graphql_introspection.py
Auke Booij fe8eabff19 server: fix the nullability of object relationships (fix hasura/graphql-engine#7201)
When adding object relationships, we set the nullability of the generated GraphQL field based on whether the database backend enforces that the referenced data always exists. For manual relationships (corresponding to `manual_configuration`), the database backend is unaware of any relationship between data, and hence such fields are always set to be nullable.

For relationships generated from foreign key constraints (corresponding to `foreign_key_constraint_on`), we distinguish between two cases:

1. The "forward" object relationship from a referencing table (i.e. which has the foreign key constraint) to a referenced table. This should be set to be non-nullable when all referencing columns are non-nullable. But in fact, it used to set it to be non-nullable if *any* referencing column is non-nullable, which is only correct in Postgres when `MATCH FULL` is set (a flag we don't consider). This fixes that by changing a boolean conjunction to a disjunction.
2. The "reverse" object relationship from a referenced table to a referencing table which has the foreign key constraint. This should always be set to be nullable. But in fact, it used to always be set to non-nullable, as was reported in hasura/graphql-engine#7201. This fixes that.

Moreover, we have moved the computation of the nullability from `Hasura.RQL.DDL.Relationship` to `Hasura.GraphQL.Schema.Select`: this nullability used to be passed through the `riIsNullable` field of `RelInfo`, but for array relationships this information is not actually used, and moreover the remaining fields of `RelInfo` are already enough to deduce the nullability.

This also adds regression tests for both (1) and (2) above.

https://github.com/hasura/graphql-engine-mono/pull/2159

GitOrigin-RevId: 617f12765614f49746d18d3368f41dfae2f3e6ca
2021-08-26 15:27:34 +00:00

123 lines
5.2 KiB
Python

import pytest
import ruamel.yaml as yaml
from validate import check_query_f, check_query
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestGraphqlIntrospection:
def test_introspection(self, hge_ctx):
with open(self.dir() + "/introspection.yaml") as c:
conf = yaml.safe_load(c)
resp, _ = check_query(hge_ctx, conf)
hasArticle = False
hasArticleAuthorFKRel = False
hasArticleAuthorManualRel = False
for t in resp['data']['__schema']['types']:
if t['name'] == 'article':
hasArticle = True
for fld in t['fields']:
if fld['name'] == 'author_obj_rel_manual':
hasArticleAuthorManualRel = True
assert fld['type']['kind'] == 'OBJECT'
elif fld['name'] == 'author_obj_rel_fk':
hasArticleAuthorFKRel = True
assert fld['type']['kind'] == 'NON_NULL'
assert hasArticle
assert hasArticleAuthorFKRel
assert hasArticleAuthorManualRel
def test_introspection_user(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/introspection_user_role.yaml")
@classmethod
def dir(cls):
return "queries/graphql_introspection"
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestNullableObjectRelationshipInSchema:
@classmethod
def dir(cls):
return "queries/graphql_introspection/nullable_object_relationship"
def test_introspection_both_directions_both_nullabilities(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/nullability.yaml")
def getTypeNameFromType(typeObject):
if typeObject['name'] != None:
return typeObject['name']
elif isinstance(typeObject['ofType'],dict):
return getTypeNameFromType(typeObject['ofType'])
else:
raise Exception("typeObject doesn't have name and ofType is not an object")
@pytest.mark.usefixtures('per_class_tests_db_state')
class TestGraphqlIntrospectionWithCustomTableName:
# test to check some of the type names that are generated
# while tracking a table with a custom name
def test_introspection(self, hge_ctx):
with open(self.dir() + "/introspection.yaml") as c:
conf = yaml.safe_load(c)
resp, _ = check_query(hge_ctx, conf)
hasMultiSelect = False
hasAggregate = False
hasSelectByPk = False
hasQueryRoot = False
for t in resp['data']['__schema']['types']:
if t['name'] == 'query_root':
hasQueryRoot = True
for field in t['fields']:
if field['name'] == 'user_address':
hasMultiSelect = True
assert 'args' in field
for args in field['args']:
if args['name'] == 'distinct_on':
assert "user_address_select_column" == getTypeNameFromType(args['type'])
elif args['name'] == 'order_by':
assert "user_address_order_by" == getTypeNameFromType(args['type'])
elif args['name'] == 'where':
assert 'user_address_bool_exp' == getTypeNameFromType(args['type'])
elif field['name'] == 'user_address_aggregate':
hasAggregate = True
assert "user_address_aggregate" == getTypeNameFromType(field['type'])
elif field['name'] == 'user_address_by_pk':
assert "user_address" == getTypeNameFromType(field['type'])
hasSelectByPk = True
elif t['name'] == 'mutation_root':
for field in t['fields']:
if field['name'] == 'insert_user_address':
hasMultiInsert = True
assert "user_address_mutation_response" == getTypeNameFromType(field['type'])
for args in field['args']:
if args['name'] == 'object':
assert "user_address_insert_input" == getTypeNameFromType(args['type'])
elif field['name'] == 'update_user_address_by_pk':
hasUpdateByPk = True
assert "user_address" == getTypeNameFromType(field['type'])
for args in field['args']:
if args['name'] == 'object':
assert "user_address" == getTypeNameFromType(args['type'])
assert hasQueryRoot
assert hasMultiSelect
assert hasAggregate
assert hasSelectByPk
assert hasMultiInsert
assert hasUpdateByPk
@classmethod
def dir(cls):
return "queries/graphql_introspection/custom_table_name"
@pytest.mark.usefixtures('per_class_tests_db_state', 'pro_tests_fixtures')
class TestDisableGraphQLIntrospection:
@classmethod
def dir(cls):
return "queries/graphql_introspection/disable_introspection"
setup_metadata_api_version = "v2"
def test_disable_introspection(self, hge_ctx):
check_query_f(hge_ctx, self.dir() + "/disable_introspection.yaml")