From d473b113c338b914860f17f59a384b68431185ca Mon Sep 17 00:00:00 2001 From: Arun Suresh Kumar Date: Wed, 12 Apr 2023 00:01:54 +0530 Subject: [PATCH 1/4] Support Async --- graphene_mongo/__init__.py | 8 +- graphene_mongo/converter.py | 234 +++- graphene_mongo/fields.py | 11 +- graphene_mongo/fields_async.py | 315 +++++ graphene_mongo/registry.py | 10 +- graphene_mongo/tests/nodes.py | 7 + graphene_mongo/tests/nodes_async.py | 107 ++ graphene_mongo/tests/test_relay_query.py | 2 + .../tests/test_relay_query_async.py | 1082 +++++++++++++++++ graphene_mongo/types.py | 12 +- graphene_mongo/types_async.py | 198 +++ graphene_mongo/utils.py | 7 +- 12 files changed, 1954 insertions(+), 39 deletions(-) create mode 100644 graphene_mongo/fields_async.py create mode 100644 graphene_mongo/tests/nodes_async.py create mode 100644 graphene_mongo/tests/test_relay_query_async.py create mode 100644 graphene_mongo/types_async.py diff --git a/graphene_mongo/__init__.py b/graphene_mongo/__init__.py index 316998ed..2a39c5f7 100644 --- a/graphene_mongo/__init__.py +++ b/graphene_mongo/__init__.py @@ -1,13 +1,17 @@ from .fields import MongoengineConnectionField +from .fields_async import AsyncMongoengineConnectionField from .types import MongoengineObjectType, MongoengineInputType, MongoengineInterfaceType +from .types_async import AsyncMongoengineObjectType __version__ = "0.1.1" __all__ = [ "__version__", "MongoengineObjectType", + "AsyncMongoengineObjectType", "MongoengineInputType", "MongoengineInterfaceType", - "MongoengineConnectionField" - ] + "MongoengineConnectionField", + "AsyncMongoengineConnectionField" +] diff --git a/graphene_mongo/converter.py b/graphene_mongo/converter.py index 5cba247c..d4ae4856 100644 --- a/graphene_mongo/converter.py +++ b/graphene_mongo/converter.py @@ -1,13 +1,15 @@ +import asyncio import sys import graphene import mongoengine +from asgiref.sync import sync_to_async from graphene.types.json import JSONString from graphene.utils.str_converters import to_snake_case, to_camel_case from mongoengine.base import get_document, LazyReference from . import advanced_types -from .utils import import_single_dispatch, get_field_description, get_query_fields +from .utils import import_single_dispatch, get_field_description, get_query_fields, ExecutorEnum from concurrent.futures import ThreadPoolExecutor, as_completed singledispatch = import_single_dispatch() @@ -18,7 +20,7 @@ class MongoEngineConversionError(Exception): @singledispatch -def convert_mongoengine_field(field, registry=None): +def convert_mongoengine_field(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): raise MongoEngineConversionError( "Don't know how to convert the MongoEngine field %s (%s)" % (field, field.__class__) @@ -28,7 +30,7 @@ def convert_mongoengine_field(field, registry=None): @convert_mongoengine_field.register(mongoengine.EmailField) @convert_mongoengine_field.register(mongoengine.StringField) @convert_mongoengine_field.register(mongoengine.URLField) -def convert_field_to_string(field, registry=None): +def convert_field_to_string(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.String( description=get_field_description(field, registry), required=field.required ) @@ -36,7 +38,7 @@ def convert_field_to_string(field, registry=None): @convert_mongoengine_field.register(mongoengine.UUIDField) @convert_mongoengine_field.register(mongoengine.ObjectIdField) -def convert_field_to_id(field, registry=None): +def convert_field_to_id(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.ID( description=get_field_description(field, registry), required=field.required ) @@ -45,14 +47,14 @@ def convert_field_to_id(field, registry=None): @convert_mongoengine_field.register(mongoengine.IntField) @convert_mongoengine_field.register(mongoengine.LongField) @convert_mongoengine_field.register(mongoengine.SequenceField) -def convert_field_to_int(field, registry=None): +def convert_field_to_int(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Int( description=get_field_description(field, registry), required=field.required ) @convert_mongoengine_field.register(mongoengine.BooleanField) -def convert_field_to_boolean(field, registry=None): +def convert_field_to_boolean(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Boolean( description=get_field_description(field, registry), required=field.required ) @@ -60,21 +62,21 @@ def convert_field_to_boolean(field, registry=None): @convert_mongoengine_field.register(mongoengine.DecimalField) @convert_mongoengine_field.register(mongoengine.FloatField) -def convert_field_to_float(field, registry=None): +def convert_field_to_float(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Float( description=get_field_description(field, registry), required=field.required ) @convert_mongoengine_field.register(mongoengine.Decimal128Field) -def convert_field_to_decimal(field, registry=None): +def convert_field_to_decimal(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Decimal( description=get_field_description(field, registry), required=field.required ) @convert_mongoengine_field.register(mongoengine.DateTimeField) -def convert_field_to_datetime(field, registry=None): +def convert_field_to_datetime(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.DateTime( description=get_field_description(field, registry), required=field.required ) @@ -82,32 +84,32 @@ def convert_field_to_datetime(field, registry=None): @convert_mongoengine_field.register(mongoengine.DictField) @convert_mongoengine_field.register(mongoengine.MapField) -def convert_field_to_jsonstring(field, registry=None): +def convert_field_to_jsonstring(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return JSONString( description=get_field_description(field, registry), required=field.required ) @convert_mongoengine_field.register(mongoengine.PointField) -def convert_point_to_field(field, registry=None): +def convert_point_to_field(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Field(advanced_types.PointFieldType, description=get_field_description(field, registry), required=field.required) @convert_mongoengine_field.register(mongoengine.PolygonField) -def convert_polygon_to_field(field, registry=None): +def convert_polygon_to_field(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Field(advanced_types.PolygonFieldType, description=get_field_description(field, registry), required=field.required) @convert_mongoengine_field.register(mongoengine.MultiPolygonField) -def convert_multipolygon_to_field(field, registry=None): +def convert_multipolygon_to_field(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Field(advanced_types.MultiPolygonFieldType, description=get_field_description(field, registry), required=field.required) @convert_mongoengine_field.register(mongoengine.FileField) -def convert_file_to_field(field, registry=None): +def convert_file_to_field(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): return graphene.Field(advanced_types.FileFieldType, description=get_field_description(field, registry), required=field.required) @@ -115,8 +117,8 @@ def convert_file_to_field(field, registry=None): @convert_mongoengine_field.register(mongoengine.ListField) @convert_mongoengine_field.register(mongoengine.EmbeddedDocumentListField) @convert_mongoengine_field.register(mongoengine.GeoPointField) -def convert_field_to_list(field, registry=None): - base_type = convert_mongoengine_field(field.field, registry=registry) +def convert_field_to_list(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): + base_type = convert_mongoengine_field(field.field, registry=registry, executor=executor) if isinstance(base_type, graphene.Field): if isinstance(field.field, mongoengine.GenericReferenceField): def get_reference_objects(*args, **kwargs): @@ -184,11 +186,78 @@ def reference_resolver(root, *args, **kwargs): return ordered_result return None + async def get_reference_objects_async(*args, **kwargs): + document = get_document(args[0]) + document_field = mongoengine.ReferenceField(document) + document_field = convert_mongoengine_field(document_field, registry, executor=ExecutorEnum.ASYNC) + document_field_type = document_field.get_type().type + queried_fields = list() + filter_args = list() + if document_field_type._meta.filter_fields: + for key, values in document_field_type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + for each in get_query_fields(args[3][0])[document_field_type._meta.name].keys(): + item = to_snake_case(each) + if item in document._fields_ordered + tuple(filter_args): + queried_fields.append(item) + return await sync_to_async(list, thread_sensitive=False, executor=ThreadPoolExecutor())( + document.objects().no_dereference().only( + *set(list(document_field_type._meta.required_fields) + queried_fields)).filter( + pk__in=args[1])) + + async def get_non_querying_object_async(*args, **kwargs): + model = get_document(args[0]) + return [model(pk=each) for each in args[1]] + + async def reference_resolver_async(root, *args, **kwargs): + to_resolve = getattr(root, field.name or field.db_name) + if to_resolve: + choice_to_resolve = dict() + querying_union_types = list(get_query_fields(args[0]).keys()) + if '__typename' in querying_union_types: + querying_union_types.remove('__typename') + to_resolve_models = list() + for each in querying_union_types: + to_resolve_models.append(registry._registry_string_map[each]) + to_resolve_object_ids = list() + for each in to_resolve: + if isinstance(each, LazyReference): + to_resolve_object_ids.append(each.pk) + model = each.document_type._class_name + if model not in choice_to_resolve: + choice_to_resolve[model] = list() + choice_to_resolve[model].append(each.pk) + else: + to_resolve_object_ids.append(each["_ref"].id) + if each['_cls'] not in choice_to_resolve: + choice_to_resolve[each['_cls']] = list() + choice_to_resolve[each['_cls']].append(each["_ref"].id) + loop = asyncio.get_event_loop() + tasks = [] + for model, object_id_list in choice_to_resolve.items(): + if model in to_resolve_models: + task = loop.create_task(get_reference_objects_async(model, object_id_list, registry, args)) + else: + task = loop.create_task( + get_non_querying_object_async(model, object_id_list, registry, args)) + tasks.append(task) + result = await asyncio.gather(*tasks) + result = [each[0] for each in result] + result_object_ids = list() + for each in result: + result_object_ids.append(each.id) + ordered_result = list() + for each in to_resolve_object_ids: + ordered_result.append(result[result_object_ids.index(each)]) + return ordered_result + return None + return graphene.List( base_type._type, description=get_field_description(field, registry), required=field.required, - resolver=reference_resolver + resolver=reference_resolver if executor == ExecutorEnum.SYNC else reference_resolver_async ) return graphene.List( base_type._type, @@ -220,7 +289,7 @@ def reference_resolver(root, *args, **kwargs): @convert_mongoengine_field.register(mongoengine.GenericEmbeddedDocumentField) @convert_mongoengine_field.register(mongoengine.GenericReferenceField) -def convert_field_to_union(field, registry=None): +def convert_field_to_union(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): _types = [] for choice in field.choices: if isinstance(field, mongoengine.GenericReferenceField): @@ -294,6 +363,56 @@ def lazy_reference_resolver(root, *args, **kwargs): return document.document_type() return None + async def reference_resolver_async(root, *args, **kwargs): + de_referenced = getattr(root, field.name or field.db_name) + if de_referenced: + document = get_document(de_referenced["_cls"]) + document_field = mongoengine.ReferenceField(document) + document_field = convert_mongoengine_field(document_field, registry, executor=ExecutorEnum.ASYNC) + _type = document_field.get_type().type + filter_args = list() + if _type._meta.filter_fields: + for key, values in _type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + querying_types = list(get_query_fields(args[0]).keys()) + if _type.__name__ in querying_types: + queried_fields = list() + for each in get_query_fields(args[0])[_type._meta.name].keys(): + item = to_snake_case(each) + if item in document._fields_ordered + tuple(filter_args): + queried_fields.append(item) + return await sync_to_async(document.objects().no_dereference().only(*list( + set(list(_type._meta.required_fields) + queried_fields))).get, thread_sensitive=False, + executor=ThreadPoolExecutor())(pk=de_referenced["_ref"].id) + return await sync_to_async(document, thread_sensitive=False, + executor=ThreadPoolExecutor())() + return None + + async def lazy_reference_resolver_async(root, *args, **kwargs): + document = getattr(root, field.name or field.db_name) + if document: + queried_fields = list() + document_field_type = registry.get_type_for_model(document.document_type) + querying_types = list(get_query_fields(args[0]).keys()) + filter_args = list() + if document_field_type._meta.filter_fields: + for key, values in document_field_type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + if document_field_type._meta.name in querying_types: + for each in get_query_fields(args[0])[document_field_type._meta.name].keys(): + item = to_snake_case(each) + if item in document.document_type._fields_ordered + tuple(filter_args): + queried_fields.append(item) + _type = registry.get_type_for_model(document.document_type) + return await sync_to_async(document.document_type.objects().no_dereference().only( + *(set((list(_type._meta.required_fields) + queried_fields)))).get, thread_sensitive=False, + executor=ThreadPoolExecutor())(pk=document.pk) + return await sync_to_async(document.document_type, thread_sensitive=False, + executor=ThreadPoolExecutor())() + return None + if isinstance(field, mongoengine.GenericLazyReferenceField): field_resolver = None required = False @@ -303,7 +422,8 @@ def lazy_reference_resolver(root, *args, **kwargs): None) if resolver_function and callable(resolver_function): field_resolver = resolver_function - return graphene.Field(_union, resolver=field_resolver if field_resolver else lazy_reference_resolver, + return graphene.Field(_union, resolver=field_resolver if field_resolver else ( + lazy_reference_resolver if executor == ExecutorEnum.SYNC else lazy_reference_resolver_async), description=get_field_description(field, registry), required=required) elif isinstance(field, mongoengine.GenericReferenceField): @@ -315,7 +435,8 @@ def lazy_reference_resolver(root, *args, **kwargs): None) if resolver_function and callable(resolver_function): field_resolver = resolver_function - return graphene.Field(_union, resolver=field_resolver if field_resolver else reference_resolver, + return graphene.Field(_union, resolver=field_resolver if field_resolver else ( + reference_resolver if executor == ExecutorEnum.SYNC else reference_resolver_async), description=get_field_description(field, registry), required=required) return graphene.Field(_union) @@ -324,7 +445,7 @@ def lazy_reference_resolver(root, *args, **kwargs): @convert_mongoengine_field.register(mongoengine.EmbeddedDocumentField) @convert_mongoengine_field.register(mongoengine.ReferenceField) @convert_mongoengine_field.register(mongoengine.CachedReferenceField) -def convert_field_to_dynamic(field, registry=None): +def convert_field_to_dynamic(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): model = field.document_type def reference_resolver(root, *args, **kwargs): @@ -365,6 +486,45 @@ def cached_reference_resolver(root, *args, **kwargs): pk=getattr(root, field.name or field.db_name)) return None + async def reference_resolver_async(root, *args, **kwargs): + document = getattr(root, field.name or field.db_name) + if document: + queried_fields = list() + _type = registry.get_type_for_model(field.document_type) + filter_args = list() + if _type._meta.filter_fields: + for key, values in _type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + for each in get_query_fields(args[0]).keys(): + item = to_snake_case(each) + if item in field.document_type._fields_ordered + tuple(filter_args): + queried_fields.append(item) + return await sync_to_async(field.document_type.objects().no_dereference().only( + *(set(list(_type._meta.required_fields) + queried_fields))).get, thread_sensitive=False, + executor=ThreadPoolExecutor())(pk=document.id) + return None + + async def cached_reference_resolver_async(root, *args, **kwargs): + if field: + queried_fields = list() + _type = registry.get_type_for_model(field.document_type) + filter_args = list() + if _type._meta.filter_fields: + for key, values in _type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + for each in get_query_fields(args[0]).keys(): + item = to_snake_case(each) + if item in field.document_type._fields_ordered + tuple(filter_args): + queried_fields.append(item) + return await sync_to_async(field.document_type.objects().no_dereference().only( + *(set( + list(_type._meta.required_fields) + queried_fields))).get, thread_sensitive=False, + executor=ThreadPoolExecutor())( + pk=getattr(root, field.name or field.db_name)) + return None + def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: @@ -381,17 +541,19 @@ def dynamic_type(): if resolver_function and callable(resolver_function): field_resolver = resolver_function if isinstance(field, mongoengine.ReferenceField): - return graphene.Field(_type, resolver=field_resolver if field_resolver else reference_resolver, + return graphene.Field(_type, resolver=field_resolver if field_resolver else ( + reference_resolver if executor == ExecutorEnum.SYNC else reference_resolver_async), description=get_field_description(field, registry), required=required) else: - return graphene.Field(_type, resolver=field_resolver if field_resolver else cached_reference_resolver, + return graphene.Field(_type, resolver=field_resolver if field_resolver else ( + cached_reference_resolver if executor == ExecutorEnum.SYNC else cached_reference_resolver_async), description=get_field_description(field, registry), required=required) return graphene.Dynamic(dynamic_type) @convert_mongoengine_field.register(mongoengine.LazyReferenceField) -def convert_lazy_field_to_dynamic(field, registry=None): +def convert_lazy_field_to_dynamic(field, registry=None, executor: ExecutorEnum = ExecutorEnum.SYNC): model = field.document_type def lazy_resolver(root, *args, **kwargs): @@ -413,6 +575,25 @@ def lazy_resolver(root, *args, **kwargs): pk=document.pk) return None + async def lazy_resolver_async(root, *args, **kwargs): + document = getattr(root, field.name or field.db_name) + if document: + queried_fields = list() + _type = registry.get_type_for_model(document.document_type) + filter_args = list() + if _type._meta.filter_fields: + for key, values in _type._meta.filter_fields.items(): + for each in values: + filter_args.append(key + "__" + each) + for each in get_query_fields(args[0]).keys(): + item = to_snake_case(each) + if item in document.document_type._fields_ordered + tuple(filter_args): + queried_fields.append(item) + return await sync_to_async(document.document_type.objects().no_dereference().only( + *(set((list(_type._meta.required_fields) + queried_fields)))).get, thread_sensitive=False, + executor=ThreadPoolExecutor())(pk=document.pk) + return None + def dynamic_type(): _type = registry.get_type_for_model(model) if not _type: @@ -427,7 +608,8 @@ def dynamic_type(): field_resolver = resolver_function return graphene.Field( _type, - resolver=field_resolver if field_resolver else lazy_resolver, + resolver=field_resolver if field_resolver else ( + lazy_resolver if executor == ExecutorEnum.SYNC else lazy_resolver_async), description=get_field_description(field, registry), required=required, ) @@ -436,7 +618,7 @@ def dynamic_type(): if sys.version_info >= (3, 6): @convert_mongoengine_field.register(mongoengine.EnumField) - def convert_field_to_enum(field, registry=None): + def convert_field_to_enum(field, registry=None, _: ExecutorEnum = ExecutorEnum.SYNC): if not registry.check_enum_already_exist(field._enum_cls): registry.register_enum(field._enum_cls) _type = registry.get_type_for_enum(field._enum_cls) diff --git a/graphene_mongo/fields.py b/graphene_mongo/fields.py index 53942c5f..41a74890 100644 --- a/graphene_mongo/fields.py +++ b/graphene_mongo/fields.py @@ -32,7 +32,7 @@ from .converter import convert_mongoengine_field, MongoEngineConversionError from .registry import get_global_registry from .utils import get_model_reference_fields, get_query_fields, find_skip_and_limit, \ - connection_from_iterables + connection_from_iterables, ExecutorEnum import pymongo PYMONGO_VERSION = tuple(pymongo.version_tuple[:2]) @@ -48,6 +48,10 @@ def __init__(self, type, *args, **kwargs): self._get_queryset = get_queryset super(MongoengineConnectionField, self).__init__(type, *args, **kwargs) + @property + def executor(self): + return ExecutorEnum.SYNC + @property def type(self): from .types import MongoengineObjectType @@ -136,7 +140,7 @@ def is_filterable(k): return False try: converted = convert_mongoengine_field( - getattr(self.model, k), self.registry + getattr(self.model, k), self.registry, self.executor ) except MongoEngineConversionError: return False @@ -401,7 +405,8 @@ def default_resolver(self, _root, info, required_fields=None, resolved=None, **a if PYMONGO_VERSION >= (3, 7): if hasattr(self.model, '_meta') and 'db_alias' in self.model._meta: - count = (mongoengine.get_db(self.model._meta['db_alias'])[self.model._get_collection_name()]).count_documents(args_copy) + count = (mongoengine.get_db(self.model._meta['db_alias'])[ + self.model._get_collection_name()]).count_documents(args_copy) else: count = (mongoengine.get_db()[self.model._get_collection_name()]).count_documents(args_copy) else: diff --git a/graphene_mongo/fields_async.py b/graphene_mongo/fields_async.py new file mode 100644 index 00000000..fed0c321 --- /dev/null +++ b/graphene_mongo/fields_async.py @@ -0,0 +1,315 @@ +from __future__ import absolute_import + +from collections import OrderedDict +from functools import partial, reduce +from typing import Coroutine + +import bson +import graphene +import mongoengine +from bson import DBRef, ObjectId +from graphene import Context +from graphene.relay import ConnectionField +from graphene.types.argument import to_arguments +from graphene.types.dynamic import Dynamic +from graphene.types.structures import Structure +from graphene.types.utils import get_type +from graphene.utils.str_converters import to_snake_case +from graphql import GraphQLResolveInfo +from graphql_relay import from_global_id, cursor_to_offset +from mongoengine import QuerySet +from mongoengine.base import get_document +from promise import Promise +from pymongo.errors import OperationFailure +from asgiref.sync import sync_to_async +from concurrent.futures import ThreadPoolExecutor + +from . import MongoengineConnectionField +from .utils import get_query_fields, find_skip_and_limit, \ + connection_from_iterables +import pymongo + +PYMONGO_VERSION = tuple(pymongo.version_tuple[:2]) + + +class AsyncMongoengineConnectionField(MongoengineConnectionField): + def __init__(self, type, *args, **kwargs): + get_queryset = kwargs.pop("get_queryset", None) + if get_queryset: + assert callable( + get_queryset + ), "Attribute `get_queryset` on {} must be callable.".format(self) + self._get_queryset = get_queryset + super(AsyncMongoengineConnectionField, self).__init__(type, *args, **kwargs) + + @property + def type(self): + from .types_async import AsyncMongoengineObjectType + + _type = super(ConnectionField, self).type + assert issubclass( + _type, AsyncMongoengineObjectType + ), "AsyncMongoengineConnectionField only accepts AsyncMongoengineObjectType types" + assert _type._meta.connection, "The type {} doesn't have a connection".format( + _type.__name__ + ) + return _type._meta.connection + + @property + def fields(self): + return super().fields + + async def default_resolver(self, _root, info, required_fields=None, resolved=None, **args): + if required_fields is None: + required_fields = list() + args = args or {} + for key, value in dict(args).items(): + if value is None: + del args[key] + if _root is not None and not resolved: + field_name = to_snake_case(info.field_name) + if not hasattr(_root, "_fields_ordered"): + if isinstance(getattr(_root, field_name, []), list): + args["pk__in"] = [r.id for r in getattr(_root, field_name, [])] + elif field_name in _root._fields_ordered and not (isinstance(_root._fields[field_name].field, + mongoengine.EmbeddedDocumentField) or + isinstance(_root._fields[field_name].field, + mongoengine.GenericEmbeddedDocumentField)): + if getattr(_root, field_name, []) is not None: + args["pk__in"] = [r.id for r in getattr(_root, field_name, [])] + + _id = args.pop('id', None) + + if _id is not None: + args['pk'] = from_global_id(_id)[-1] + iterables = [] + list_length = 0 + skip = 0 + count = 0 + limit = None + reverse = False + first = args.pop("first", None) + after = args.pop("after", None) + if after: + after = cursor_to_offset(after) + last = args.pop("last", None) + before = args.pop("before", None) + if before: + before = cursor_to_offset(before) + + if resolved is not None: + items = resolved + + if isinstance(items, QuerySet): + try: + count = await sync_to_async(items.count, thread_sensitive=False, + executor=ThreadPoolExecutor())(with_limit_and_skip=True) + except OperationFailure: + count = len(items) + else: + count = len(items) + + skip, limit, reverse = find_skip_and_limit(first=first, last=last, after=after, before=before, + count=count) + + if limit: + if reverse: + items = items[::-1][skip:skip + limit] + else: + items = items[skip:skip + limit] + elif skip: + items = items[skip:] + iterables = items + list_length = len(iterables) + + elif callable(getattr(self.model, "objects", None)): + if _root is None or args or isinstance(getattr(_root, field_name, []), AsyncMongoengineConnectionField): + args_copy = args.copy() + for key in args.copy(): + if key not in self.model._fields_ordered: + args_copy.pop(key) + elif isinstance(getattr(self.model, key), + mongoengine.fields.ReferenceField) or isinstance(getattr(self.model, key), + mongoengine.fields.GenericReferenceField) or isinstance( + getattr(self.model, key), + mongoengine.fields.LazyReferenceField) or isinstance(getattr(self.model, key), + mongoengine.fields.CachedReferenceField): + if not isinstance(args_copy[key], ObjectId): + _from_global_id = from_global_id(args_copy[key])[1] + if bson.objectid.ObjectId.is_valid(_from_global_id): + args_copy[key] = ObjectId(_from_global_id) + else: + args_copy[key] = _from_global_id + elif isinstance(getattr(self.model, key), + mongoengine.fields.EnumField): + if getattr(args_copy[key], "value", None): + args_copy[key] = args_copy[key].value + + if PYMONGO_VERSION >= (3, 7): + count = await sync_to_async( + (mongoengine.get_db()[self.model._get_collection_name()]).count_documents, + thread_sensitive=False, + executor=ThreadPoolExecutor())(args_copy) + else: + count = await sync_to_async(self.model.objects(args_copy).count, thread_sensitive=False, + executor=ThreadPoolExecutor())() + if count != 0: + skip, limit, reverse = find_skip_and_limit(first=first, after=after, last=last, before=before, + count=count) + iterables = self.get_queryset(self.model, info, required_fields, skip, limit, reverse, **args) + list_length = len(iterables) + if isinstance(info, GraphQLResolveInfo): + if not info.context: + info = info._replace(context=Context()) + info.context.queryset = self.get_queryset(self.model, info, required_fields, **args) + + elif "pk__in" in args and args["pk__in"]: + count = len(args["pk__in"]) + skip, limit, reverse = find_skip_and_limit(first=first, last=last, after=after, before=before, + count=count) + if limit: + if reverse: + args["pk__in"] = args["pk__in"][::-1][skip:skip + limit] + else: + args["pk__in"] = args["pk__in"][skip:skip + limit] + elif skip: + args["pk__in"] = args["pk__in"][skip:] + iterables = self.get_queryset(self.model, info, required_fields, **args) + list_length = len(iterables) + if isinstance(info, GraphQLResolveInfo): + if not info.context: + info = info._replace(context=Context()) + info.context.queryset = self.get_queryset(self.model, info, required_fields, **args) + + elif _root is not None: + field_name = to_snake_case(info.field_name) + items = getattr(_root, field_name, []) + count = len(items) + skip, limit, reverse = find_skip_and_limit(first=first, last=last, after=after, before=before, + count=count) + if limit: + if reverse: + items = items[::-1][skip:skip + limit] + else: + items = items[skip:skip + limit] + elif skip: + items = items[skip:] + iterables = items + list_length = len(iterables) + + has_next_page = True if (0 if limit is None else limit) + (0 if skip is None else skip) < count else False + has_previous_page = True if skip else False + if reverse: + iterables = list(iterables) + iterables.reverse() + skip = limit + connection = connection_from_iterables(edges=iterables, start_offset=skip, + has_previous_page=has_previous_page, + has_next_page=has_next_page, + connection_type=self.type, + edge_type=self.type.Edge, + pageinfo_type=graphene.PageInfo) + + connection.iterable = iterables + connection.list_length = list_length + return connection + + async def chained_resolver(self, resolver, is_partial, root, info, **args): + + for key, value in dict(args).items(): + if value is None: + del args[key] + + required_fields = list() + + for field in self.required_fields: + if field in self.model._fields_ordered: + required_fields.append(field) + + for field in get_query_fields(info): + if to_snake_case(field) in self.model._fields_ordered: + required_fields.append(to_snake_case(field)) + + args_copy = args.copy() + + if not bool(args) or not is_partial: + if isinstance(self.model, mongoengine.Document) or isinstance(self.model, + mongoengine.base.metaclasses.TopLevelDocumentMetaclass): + + from itertools import filterfalse + connection_fields = [field for field in self.fields if + type(self.fields[field]) == AsyncMongoengineConnectionField] + filterable_args = tuple(filterfalse(connection_fields.__contains__, list(self.model._fields_ordered))) + for arg_name, arg in args.copy().items(): + if arg_name not in filterable_args + tuple(self.filter_args.keys()): + args_copy.pop(arg_name) + if isinstance(info, GraphQLResolveInfo): + if not info.context: + info = info._replace(context=Context()) + info.context.queryset = self.get_queryset(self.model, info, required_fields, **args_copy) + + # XXX: Filter nested args + resolved = resolver(root, info, **args) + if isinstance(resolved, Coroutine): + resolved = await resolved + if resolved is not None: + # if isinstance(resolved, Coroutine): + # resolved = await resolved + if isinstance(resolved, list): + if resolved == list(): + return resolved + elif not isinstance(resolved[0], DBRef): + return resolved + else: + return await self.default_resolver(root, info, required_fields, **args_copy) + elif isinstance(resolved, QuerySet): + args.update(resolved._query) + args_copy = args.copy() + for arg_name, arg in args.copy().items(): + if "." in arg_name or arg_name not in self.model._fields_ordered + ( + 'first', 'last', 'before', 'after') + tuple(self.filter_args.keys()): + args_copy.pop(arg_name) + if arg_name == '_id' and isinstance(arg, dict): + operation = list(arg.keys())[0] + args_copy['pk' + operation.replace('$', '__')] = arg[operation] + if not isinstance(arg, ObjectId) and '.' in arg_name: + if type(arg) == dict: + operation = list(arg.keys())[0] + args_copy[arg_name.replace('.', '__') + operation.replace('$', '__')] = arg[ + operation] + else: + args_copy[arg_name.replace('.', '__')] = arg + elif '.' in arg_name and isinstance(arg, ObjectId): + args_copy[arg_name.replace('.', '__')] = arg + else: + operations = ["$lte", "$gte", "$ne", "$in"] + if isinstance(arg, dict) and any(op in arg for op in operations): + operation = list(arg.keys())[0] + args_copy[arg_name + operation.replace('$', '__')] = arg[operation] + del args_copy[arg_name] + + return await self.default_resolver(root, info, required_fields, resolved=resolved, **args_copy) + elif isinstance(resolved, Promise): + return resolved.value + else: + return await resolved + + return await self.default_resolver(root, info, required_fields, **args) + + async def connection_resolver(cls, resolver, connection_type, root, info, **args): + if root: + for key, value in root.__dict__.items(): + if value: + try: + setattr(root, key, from_global_id(value)[1]) + except Exception: + pass + iterable = await resolver(root, info, **args) + if isinstance(connection_type, graphene.NonNull): + connection_type = connection_type.of_type + if Promise.is_thenable(iterable): + # on_resolve = partial(cls.resolve_connection, connection_type, args) + iterable = Promise.resolve(iterable).value + + return await sync_to_async(cls.resolve_connection, thread_sensitive=False, + executor=ThreadPoolExecutor())(connection_type, args, iterable) diff --git a/graphene_mongo/registry.py b/graphene_mongo/registry.py index 8d57713e..a7aa61d3 100644 --- a/graphene_mongo/registry.py +++ b/graphene_mongo/registry.py @@ -9,11 +9,15 @@ def __init__(self): def register(self, cls): from .types import GrapheneMongoengineObjectTypes + from .types_async import AsyncGrapheneMongoengineObjectTypes - assert issubclass( + assert (issubclass( cls, GrapheneMongoengineObjectTypes - ), 'Only Mongoengine object types can be registered, received "{}"'.format( + ) or issubclass( + cls, + AsyncGrapheneMongoengineObjectTypes + )), 'Only Mongoengine/Async Mongoengine object types can be registered, received "{}"'.format( cls.__name__ ) assert cls._meta.registry == self, "Registry for a Model have to match." @@ -47,7 +51,9 @@ def get_type_for_enum(self, cls): registry = None +async_registry = None inputs_registry = None +async_inputs_registry = None def get_inputs_registry(): diff --git a/graphene_mongo/tests/nodes.py b/graphene_mongo/tests/nodes.py index 3be5759a..3053f300 100644 --- a/graphene_mongo/tests/nodes.py +++ b/graphene_mongo/tests/nodes.py @@ -3,6 +3,7 @@ from . import models from . import types # noqa: F401 +from ..types_async import AsyncMongoengineObjectType from ..types import MongoengineObjectType @@ -47,6 +48,12 @@ class Meta: interfaces = (Node,) +class ReporterNodeAsync(AsyncMongoengineObjectType): + class Meta: + model = models.Reporter + interfaces = (Node,) + + class ParentNode(MongoengineObjectType): class Meta: model = models.Parent diff --git a/graphene_mongo/tests/nodes_async.py b/graphene_mongo/tests/nodes_async.py new file mode 100644 index 00000000..ac7fb95e --- /dev/null +++ b/graphene_mongo/tests/nodes_async.py @@ -0,0 +1,107 @@ +import graphene +from graphene.relay import Node + +from . import models +from . import types # noqa: F401 +from ..types_async import AsyncMongoengineObjectType + + +class PublisherNode(AsyncMongoengineObjectType): + legal_name = graphene.String() + bad_field = graphene.String() + + class Meta: + model = models.Publisher + only_fields = ("id", "name") + interfaces = (Node,) + + +class ArticleNode(AsyncMongoengineObjectType): + class Meta: + model = models.Article + interfaces = (Node,) + + +class EditorNode(AsyncMongoengineObjectType): + class Meta: + model = models.Editor + interfaces = (Node,) + + +class EmbeddedArticleNode(AsyncMongoengineObjectType): + class Meta: + model = models.EmbeddedArticle + interfaces = (Node,) + + +class PlayerNode(AsyncMongoengineObjectType): + class Meta: + model = models.Player + interfaces = (Node,) + filter_fields = {"first_name": ["istartswith", "in"]} + + +class ReporterNode(AsyncMongoengineObjectType): + class Meta: + model = models.Reporter + interfaces = (Node,) + + +class ReporterNodeAsync(AsyncMongoengineObjectType): + class Meta: + model = models.Reporter + interfaces = (Node,) + + +class ParentNode(AsyncMongoengineObjectType): + class Meta: + model = models.Parent + interfaces = (Node,) + + +class ChildNode(AsyncMongoengineObjectType): + class Meta: + model = models.Child + interfaces = (Node,) + + +class ChildRegisteredBeforeNode(AsyncMongoengineObjectType): + class Meta: + model = models.ChildRegisteredBefore + interfaces = (Node,) + + +class ParentWithRelationshipNode(AsyncMongoengineObjectType): + class Meta: + model = models.ParentWithRelationship + interfaces = (Node,) + + +class ChildRegisteredAfterNode(AsyncMongoengineObjectType): + class Meta: + model = models.ChildRegisteredAfter + interfaces = (Node,) + + +class ProfessorVectorNode(AsyncMongoengineObjectType): + class Meta: + model = models.ProfessorVector + interfaces = (Node,) + + +class ErroneousModelNode(AsyncMongoengineObjectType): + class Meta: + model = models.ErroneousModel + interfaces = (Node,) + + +class BarNode(AsyncMongoengineObjectType): + class Meta: + model = models.Bar + interfaces = (Node,) + + +class FooNode(AsyncMongoengineObjectType): + class Meta: + model = models.Foo + interfaces = (Node,) diff --git a/graphene_mongo/tests/test_relay_query.py b/graphene_mongo/tests/test_relay_query.py index 5e181861..9824477a 100644 --- a/graphene_mongo/tests/test_relay_query.py +++ b/graphene_mongo/tests/test_relay_query.py @@ -2,12 +2,14 @@ import json import base64 import graphene +import pytest from graphene.relay import Node from graphql_relay.node.node import to_global_id from . import models from . import nodes +from .. import AsyncMongoengineConnectionField from ..fields import MongoengineConnectionField from ..types import MongoengineObjectType diff --git a/graphene_mongo/tests/test_relay_query_async.py b/graphene_mongo/tests/test_relay_query_async.py new file mode 100644 index 00000000..71c66b34 --- /dev/null +++ b/graphene_mongo/tests/test_relay_query_async.py @@ -0,0 +1,1082 @@ +import os +import json +import base64 +import graphene +import pytest + +from graphene.relay import Node +from graphql_relay.node.node import to_global_id + +from . import models +from . import nodes_async +from .. import AsyncMongoengineConnectionField, AsyncMongoengineObjectType +from ..fields import MongoengineConnectionField +from ..types import MongoengineObjectType + + +@pytest.mark.asyncio +async def test_should_query_reporter_async(fixtures): + class Query(graphene.ObjectType): + reporter = graphene.Field(nodes_async.ReporterNode) + + async def resolve_reporter(self, *args, **kwargs): + return models.Reporter.objects.no_dereference().first() + + query = """ + query ReporterQuery { + reporter { + firstName, + lastName, + email, + awards, + articles { + edges { + node { + headline + } + } + }, + embeddedArticles { + edges { + node { + headline + } + } + }, + embeddedListArticles { + edges { + node { + headline + } + } + }, + genericReference { + __typename + ... on ArticleNode { + headline + } + } + } + } + """ + expected = { + "reporter": { + "firstName": "Allen", + "lastName": "Iverson", + "email": "ai@gmail.com", + "awards": ["2010-mvp"], + "articles": { + "edges": [ + {"node": {"headline": "Hello"}}, + {"node": {"headline": "World"}}, + ] + }, + "embeddedArticles": { + "edges": [ + {"node": {"headline": "Real"}}, + {"node": {"headline": "World"}}, + ] + }, + "embeddedListArticles": { + "edges": [ + {"node": {"headline": "World"}}, + {"node": {"headline": "Real"}}, + ] + }, + "genericReference": {"__typename": "ArticleNode", "headline": "Hello"}, + } + } + + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_query_reporters_with_nested_document_async(fixtures): + class Query(graphene.ObjectType): + reporters = AsyncMongoengineConnectionField(nodes_async.ReporterNode) + + query = """ + query ReporterQuery { + reporters(firstName: "Allen") { + edges { + node { + firstName, + lastName, + email, + articles(headline: "Hello") { + edges { + node { + headline + } + } + } + } + } + } + } + """ + expected = { + "reporters": { + "edges": [ + { + "node": { + "firstName": "Allen", + "lastName": "Iverson", + "email": "ai@gmail.com", + "articles": {"edges": [{"node": {"headline": "Hello"}}]}, + } + } + ] + } + } + + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_query_all_editors_async(fixtures, fixtures_dirname): + class Query(graphene.ObjectType): + editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + + query = """ + query EditorQuery { + editors { + edges { + node { + id, + firstName, + lastName, + avatar { + contentType, + length, + data + } + } + } + } + } + """ + + avator_filename = os.path.join(fixtures_dirname, "image.jpg") + with open(avator_filename, "rb") as f: + data = base64.b64encode(f.read()) + + expected = { + "editors": { + "edges": [ + { + "node": { + "id": "RWRpdG9yTm9kZTox", + "firstName": "Penny", + "lastName": "Hardaway", + "avatar": { + "contentType": "image/jpeg", + "length": 46928, + "data": data.decode("utf-8"), + }, + } + }, + { + "node": { + "id": "RWRpdG9yTm9kZToy", + "firstName": "Grant", + "lastName": "Hill", + "avatar": {"contentType": None, "length": 0, "data": None}, + } + }, + { + "node": { + "id": "RWRpdG9yTm9kZToz", + "firstName": "Dennis", + "lastName": "Rodman", + "avatar": {"contentType": None, "length": 0, "data": None}, + } + }, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_query_editors_with_dataloader_async(fixtures): + from promise import Promise + from promise.dataloader import DataLoader + + class ArticleLoader(DataLoader): + async def batch_load_fn(self, instances): + queryset = models.Article.objects(editor__in=instances) + return Promise.resolve( + [ + [a for a in queryset if a.editor.id == instance.id] + for instance in instances + ] + ) + + article_loader = ArticleLoader() + + class _EditorNode(AsyncMongoengineObjectType): + class Meta: + model = models.Editor + interfaces = (graphene.Node,) + + articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + + async def resolve_articles(self, *args, **kwargs): + return article_loader.load(self) + + class Query(graphene.ObjectType): + editors = AsyncMongoengineConnectionField(_EditorNode) + + query = """ + query EditorPromiseConnectionQuery { + editors(first: 1) { + edges { + node { + firstName, + articles(first: 1) { + edges { + node { + headline + } + } + } + } + } + } + } + """ + + expected = { + "editors": { + "edges": [ + { + "node": { + "firstName": "Penny", + "articles": {"edges": [{"node": {"headline": "Hello"}}]}, + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_editors_by_id_async(fixtures): + class Query(graphene.ObjectType): + editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + + query = """ + query EditorQuery { + editors(id: "RWRpdG9yTm9kZToy") { + edges { + node { + id, + firstName, + lastName + } + } + } + } + """ + expected = { + "editors": { + "edges": [ + { + "node": { + "id": "RWRpdG9yTm9kZToy", + "firstName": "Grant", + "lastName": "Hill", + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_async(fixtures): + class Query(graphene.ObjectType): + articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + + query = """ + query ArticlesQuery { + articles(headline: "World") { + edges { + node { + headline, + pubDate, + editor { + firstName + } + } + } + } + } + """ + expected = { + "articles": { + "edges": [ + { + "node": { + "headline": "World", + "editor": {"firstName": "Grant"}, + "pubDate": "2020-01-01T00:00:00", + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_by_reference_field_async(fixtures): + class Query(graphene.ObjectType): + articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + + query = """ + query ArticlesQuery { + articles(editor: "RWRpdG9yTm9kZTox") { + edges { + node { + headline, + editor { + firstName + } + } + } + } + } + """ + expected = { + "articles": { + "edges": [{"node": {"headline": "Hello", "editor": {"firstName": "Penny"}}}] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_through_inheritance_async(fixtures): + class Query(graphene.ObjectType): + node = Node.Field() + children = AsyncMongoengineConnectionField(nodes_async.ChildNode) + + query = """ + query ChildrenQuery { + children(bar: "bar") { + edges { + node { + bar, + baz, + loc { + type, + coordinates + } + } + } + } + } + """ + expected = { + "children": { + "edges": [ + { + "node": { + "bar": "bar", + "baz": "baz", + "loc": {"type": "Point", "coordinates": [10.0, 20.0]}, + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_by_list_contains_async(fixtures): + # Notes: https://goo.gl/hMNRgs + class Query(graphene.ObjectType): + reporters = AsyncMongoengineConnectionField(nodes_async.ReporterNodeAsync) + + query = """ + query ReportersQuery { + reporters (awards: "2010-mvp") { + edges { + node { + id, + firstName, + awards, + genericReferences { + __typename + ... on ArticleNode { + headline + } + } + } + } + } + } + """ + expected = { + "reporters": { + "edges": [ + { + "node": { + "id": "UmVwb3J0ZXJOb2RlQXN5bmM6MQ==", + "firstName": "Allen", + "awards": ["2010-mvp"], + "genericReferences": [ + { + "__typename": "ArticleNode", + "headline": "Hello" + } + ] + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_by_id_async(fixtures): + # Notes: https://goo.gl/hMNRgs + class Query(graphene.ObjectType): + reporter = Node.Field(nodes_async.ReporterNode) + + query = """ + query ReporterQuery { + reporter (id: "UmVwb3J0ZXJOb2RlOjE=") { + id, + firstName, + awards + } + } + """ + expected = { + "reporter": { + "id": "UmVwb3J0ZXJOb2RlOjE=", + "firstName": "Allen", + "awards": ["2010-mvp"], + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_first_n_async(fixtures): + class Query(graphene.ObjectType): + editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + + query = """ + query EditorQuery { + editors(first: 2) { + edges { + cursor, + node { + firstName + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } + """ + expected = { + "editors": { + "edges": [ + {"cursor": "YXJyYXljb25uZWN0aW9uOjA=", "node": {"firstName": "Penny"}}, + {"cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": {"firstName": "Grant"}}, + ], + "pageInfo": { + "hasNextPage": True, + "hasPreviousPage": False, + "startCursor": "YXJyYXljb25uZWN0aW9uOjA=", + "endCursor": "YXJyYXljb25uZWN0aW9uOjE=", + }, + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_after_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query EditorQuery { + players(after: "YXJyYXljb25uZWN0aW9uOjA=") { + edges { + cursor, + node { + firstName + } + } + } + } + """ + expected = { + "players": { + "edges": [ + {"cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": {"firstName": "Magic"}}, + {"cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": {"firstName": "Larry"}}, + {"cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": {"firstName": "Chris"}}, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_before_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query EditorQuery { + players(before: "YXJyYXljb25uZWN0aW9uOjI=") { + edges { + cursor, + node { + firstName + } + } + } + } + """ + expected = { + "players": { + "edges": [ + { + "cursor": "YXJyYXljb25uZWN0aW9uOjA=", + "node": {"firstName": "Michael"}, + }, + {"cursor": "YXJyYXljb25uZWN0aW9uOjE=", "node": {"firstName": "Magic"}}, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_last_n_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query PlayerQuery { + players(last: 2) { + edges { + cursor, + node { + firstName + } + } + } + } + """ + expected = { + "players": { + "edges": [ + {"cursor": "YXJyYXljb25uZWN0aW9uOjI=", "node": {"firstName": "Larry"}}, + {"cursor": "YXJyYXljb25uZWN0aW9uOjM=", "node": {"firstName": "Chris"}}, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_self_reference_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query PlayersQuery { + players { + edges { + node { + firstName, + players { + edges { + node { + firstName + } + } + }, + embeddedListArticles { + edges { + node { + headline + } + } + } + } + } + } + } + """ + expected = { + "players": { + "edges": [ + { + "node": { + "firstName": "Michael", + "players": {"edges": [{"node": {"firstName": "Magic"}}]}, + "embeddedListArticles": {"edges": []}, + } + }, + { + "node": { + "firstName": "Magic", + "players": {"edges": [{"node": {"firstName": "Michael"}}]}, + "embeddedListArticles": {"edges": []}, + } + }, + { + "node": { + "firstName": "Larry", + "players": { + "edges": [ + {"node": {"firstName": "Michael"}}, + {"node": {"firstName": "Magic"}}, + ] + }, + "embeddedListArticles": {"edges": []}, + } + }, + { + "node": { + "firstName": "Chris", + "players": {"edges": []}, + "embeddedListArticles": {"edges": []}, + } + }, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_lazy_reference_async(fixtures): + class Query(graphene.ObjectType): + node = Node.Field() + parents = AsyncMongoengineConnectionField(nodes_async.ParentWithRelationshipNode) + + schema = graphene.Schema(query=Query) + + query = """ + query { + parents { + edges { + node { + beforeChild { + edges { + node { + name, + parent { name } + } + } + }, + afterChild { + edges { + node { + name, + parent { name } + } + } + } + } + } + } + } + """ + + expected = { + "parents": { + "edges": [ + { + "node": { + "beforeChild": { + "edges": [ + {"node": {"name": "Akari", "parent": {"name": "Yui"}}} + ] + }, + "afterChild": { + "edges": [ + {"node": {"name": "Kyouko", "parent": {"name": "Yui"}}} + ] + }, + } + } + ] + } + } + + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_query_with_embedded_document_async(fixtures): + class Query(graphene.ObjectType): + professors = AsyncMongoengineConnectionField(nodes_async.ProfessorVectorNode) + + query = """ + query { + professors { + edges { + node { + vec, + metadata { + firstName + } + } + } + } + } + """ + expected = { + "professors": { + "edges": [ + {"node": {"vec": [1.0, 2.3], "metadata": {"firstName": "Steven"}}} + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_get_queryset_returns_dict_filters_async(fixtures): + class Query(graphene.ObjectType): + node = Node.Field() + articles = AsyncMongoengineConnectionField( + nodes_async.ArticleNode, get_queryset=lambda *_, **__: {"headline": "World"} + ) + + query = """ + query ArticlesQuery { + articles { + edges { + node { + headline, + pubDate, + editor { + firstName + } + } + } + } + } + """ + expected = { + "articles": { + "edges": [ + { + "node": { + "headline": "World", + "editor": {"firstName": "Grant"}, + "pubDate": "2020-01-01T00:00:00", + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_get_queryset_returns_qs_filters_async(fixtures): + async def get_queryset(model, info, **args): + return model.objects(headline="World") + + class Query(graphene.ObjectType): + node = Node.Field() + articles = AsyncMongoengineConnectionField( + nodes_async.ArticleNode, get_queryset=get_queryset + ) + + query = """ + query ArticlesQuery { + articles { + edges { + node { + headline, + pubDate, + editor { + firstName + } + } + } + } + } + """ + expected = { + "articles": { + "edges": [ + { + "node": { + "headline": "World", + "editor": {"firstName": "Grant"}, + "pubDate": "2020-01-01T00:00:00", + } + } + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_mongoengine_queryset_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query players { + players(firstName_Istartswith: "M") { + edges { + node { + firstName + } + } + } + } + """ + expected = { + "players": { + "edges": [ + {"node": {"firstName": "Michael"}}, + {"node": {"firstName": "Magic"}}, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert json.dumps(result.data, sort_keys=True) == json.dumps( + expected, sort_keys=True + ) + + +@pytest.mark.asyncio +async def test_should_query_document_with_embedded_async(fixtures): + class Query(graphene.ObjectType): + foos = AsyncMongoengineConnectionField(nodes_async.FooNode) + + async def resolve_multiple_foos(self, *args, **kwargs): + return list(models.Foo.objects.all()) + + query = """ + query { + foos { + edges { + node { + bars { + edges { + node { + someListField + } + } + } + } + } + } + } + """ + + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + assert not result.errors + + +@pytest.mark.asyncio +async def test_should_filter_mongoengine_queryset_with_list_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query players { + players(firstName_In: ["Michael", "Magic"]) { + edges { + node { + firstName + } + } + } + } + """ + expected = { + "players": { + "edges": [ + {"node": {"firstName": "Michael"}}, + {"node": {"firstName": "Magic"}}, + ] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert json.dumps(result.data, sort_keys=True) == json.dumps( + expected, sort_keys=True + ) + + +@pytest.mark.asyncio +async def test_should_get_correct_list_of_documents_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + query = """ + query players { + players(firstName: "Michael") { + edges { + node { + firstName, + articles(first: 3) { + edges { + node { + headline + } + } + } + } + } + } + } + """ + expected = { + "players": { + "edges": [{ + "node": { + "firstName": "Michael", + "articles": { + "edges": [{ + "node": { + "headline": "Hello" + } + }, { + "node": { + "headline": "World" + } + }] + } + } + }] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert result.data == expected + + +@pytest.mark.asyncio +async def test_should_filter_mongoengine_queryset_by_id_and_other_fields_async(fixtures): + class Query(graphene.ObjectType): + players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + + larry = models.Player.objects.get(first_name="Larry") + larry_relay_id = to_global_id("PlayerNode", larry.id) + + # "Larry" id && firstName == "Michael" should return nothing + query = """ + query players {{ + players( + id: "{larry_relay_id}", + firstName: "Michael" + ) {{ + edges {{ + node {{ + id + firstName + }} + }} + }} + }} + """.format(larry_relay_id=larry_relay_id) + + expected = { + 'players': { + 'edges': [] + } + } + schema = graphene.Schema(query=Query) + result = await schema.execute_async(query) + + assert not result.errors + assert json.dumps(result.data, sort_keys=True) == json.dumps(expected, sort_keys=True) diff --git a/graphene_mongo/types.py b/graphene_mongo/types.py index 71a34ab2..d3030ba0 100644 --- a/graphene_mongo/types.py +++ b/graphene_mongo/types.py @@ -12,16 +12,18 @@ from .converter import convert_mongoengine_field from .registry import Registry, get_global_registry, get_inputs_registry -from .utils import get_model_fields, is_valid_mongoengine_model, get_query_fields +from .utils import get_model_fields, is_valid_mongoengine_model, get_query_fields, ExecutorEnum -def construct_fields(model, registry, only_fields, exclude_fields, non_required_fields): +def construct_fields(model, registry, only_fields, exclude_fields, non_required_fields, + executor: ExecutorEnum = ExecutorEnum.SYNC): """ Args: model (mongoengine.Document): registry (.registry.Registry): only_fields ([str]): exclude_fields ([str]): + executor : ExecutorEnum Returns: (OrderedDict, OrderedDict): converted fields and self reference fields. @@ -49,7 +51,7 @@ def construct_fields(model, registry, only_fields, exclude_fields, non_required_ ): self_referenced[name] = field continue - converted = convert_mongoengine_field(field, registry) + converted = convert_mongoengine_field(field, registry, executor) if not converted: continue else: @@ -60,10 +62,10 @@ def construct_fields(model, registry, only_fields, exclude_fields, non_required_ return fields, self_referenced -def construct_self_referenced_fields(self_referenced, registry): +def construct_self_referenced_fields(self_referenced, registry, executor=ExecutorEnum.SYNC): fields = OrderedDict() for name, field in self_referenced.items(): - converted = convert_mongoengine_field(field, registry) + converted = convert_mongoengine_field(field, registry, executor) if not converted: continue fields[name] = converted diff --git a/graphene_mongo/types_async.py b/graphene_mongo/types_async.py new file mode 100644 index 00000000..27d9cc8d --- /dev/null +++ b/graphene_mongo/types_async.py @@ -0,0 +1,198 @@ +from collections import OrderedDict + +import graphene +import mongoengine +from asgiref.sync import sync_to_async +from graphene import InputObjectType +from graphene.relay import Connection, Node +from graphene.types.objecttype import ObjectType, ObjectTypeOptions +from graphene.types.utils import yank_fields_from_attrs +from graphene.utils.str_converters import to_snake_case +from graphene_mongo import AsyncMongoengineConnectionField + +from .registry import Registry, get_global_registry, \ + get_inputs_registry +from .types import construct_fields, construct_self_referenced_fields +from .utils import is_valid_mongoengine_model, get_query_fields, ExecutorEnum + + +def create_graphene_generic_class_async(object_type, option_type): + class AsyncMongoengineGenericObjectTypeOptions(option_type): + + model = None + registry = None # type: Registry + connection = None + filter_fields = () + non_required_fields = () + order_by = None + + class AsyncGrapheneMongoengineGenericType(object_type): + @classmethod + def __init_subclass_with_meta__( + cls, + model=None, + registry=None, + skip_registry=False, + only_fields=(), + required_fields=(), + exclude_fields=(), + non_required_fields=(), + filter_fields=None, + non_filter_fields=(), + connection=None, + connection_class=None, + use_connection=None, + connection_field_class=None, + interfaces=(), + _meta=None, + order_by=None, + **options + ): + + assert is_valid_mongoengine_model(model), ( + "The attribute model in {}.Meta must be a valid Mongoengine Model. " + 'Received "{}" instead.' + ).format(cls.__name__, type(model)) + + if not registry: + # input objects shall be registred in a separated registry + if issubclass(cls, InputObjectType): + registry = get_inputs_registry() + else: + registry = get_global_registry() + + assert isinstance(registry, Registry), ( + "The attribute registry in {}.Meta needs to be an instance of " + 'Registry({}), received "{}".' + ).format(object_type, cls.__name__, registry) + converted_fields, self_referenced = construct_fields( + model, registry, only_fields, exclude_fields, non_required_fields, ExecutorEnum.ASYNC + ) + mongoengine_fields = yank_fields_from_attrs( + converted_fields, _as=graphene.Field + ) + if use_connection is None and interfaces: + use_connection = any( + (issubclass(interface, Node) for interface in interfaces) + ) + + if use_connection and not connection: + # We create the connection automatically + if not connection_class: + connection_class = Connection + + connection = connection_class.create_type( + "{}Connection".format(options.get('name') or cls.__name__), node=cls + ) + + if connection is not None: + assert issubclass(connection, Connection), ( + "The attribute connection in {}.Meta must be of type Connection. " + 'Received "{}" instead.' + ).format(cls.__name__, type(connection)) + + if connection_field_class is not None: + assert issubclass(connection_field_class, graphene.ConnectionField), ( + "The attribute connection_field_class in {}.Meta must be of type graphene.ConnectionField. " + 'Received "{}" instead.' + ).format(cls.__name__, type(connection_field_class)) + else: + connection_field_class = AsyncMongoengineConnectionField + + if _meta: + assert isinstance(_meta, AsyncMongoengineGenericObjectTypeOptions), ( + "_meta must be an instance of AsyncMongoengineGenericObjectTypeOptions, " + "received {}" + ).format(_meta.__class__) + else: + _meta = AsyncMongoengineGenericObjectTypeOptions(option_type) + + _meta.model = model + _meta.registry = registry + _meta.fields = mongoengine_fields + _meta.filter_fields = filter_fields + _meta.non_filter_fields = non_filter_fields + _meta.connection = connection + _meta.connection_field_class = connection_field_class + # Save them for later + _meta.only_fields = only_fields + _meta.required_fields = required_fields + _meta.exclude_fields = exclude_fields + _meta.non_required_fields = non_required_fields + _meta.order_by = order_by + + super(AsyncGrapheneMongoengineGenericType, cls).__init_subclass_with_meta__( + _meta=_meta, interfaces=interfaces, **options + ) + + if not skip_registry: + registry.register(cls) + # Notes: Take care list of self-reference fields. + converted_fields = construct_self_referenced_fields( + self_referenced, registry, ExecutorEnum.ASYNC + ) + if converted_fields: + mongoengine_fields = yank_fields_from_attrs( + converted_fields, _as=graphene.Field + ) + cls._meta.fields.update(mongoengine_fields) + registry.register(cls) + + @classmethod + def rescan_fields(cls): + """Attempts to rescan fields and will insert any not converted initially""" + + converted_fields, self_referenced = construct_fields( + cls._meta.model, + cls._meta.registry, + cls._meta.only_fields, + cls._meta.exclude_fields, + cls._meta.non_required_fields, ExecutorEnum.ASYNC + ) + + mongoengine_fields = yank_fields_from_attrs( + converted_fields, _as=graphene.Field + ) + + # The initial scan should take precedence + for field in mongoengine_fields: + if field not in cls._meta.fields: + cls._meta.fields.update({field: mongoengine_fields[field]}) + # Self-referenced fields can't change between scans! + + @classmethod + def is_type_of(cls, root, info): + if isinstance(root, cls): + return True + # XXX: Take care FileField + if isinstance(root, mongoengine.GridFSProxy): + return True + if not is_valid_mongoengine_model(type(root)): + raise Exception(('Received incompatible instance "{}".').format(root)) + return isinstance(root, cls._meta.model) + + @classmethod + async def get_node(cls, info, id): + required_fields = list() + for field in cls._meta.required_fields: + if field in cls._meta.model._fields_ordered: + required_fields.append(field) + queried_fields = get_query_fields(info) + if cls._meta.name in queried_fields: + queried_fields = queried_fields[cls._meta.name] + for field in queried_fields: + if to_snake_case(field) in cls._meta.model._fields_ordered: + required_fields.append(to_snake_case(field)) + required_fields = list(set(required_fields)) + return await sync_to_async(cls._meta.model.objects.no_dereference().only(*required_fields).get)(pk=id) + + def resolve_id(self, info): + return str(self.id) + + return AsyncGrapheneMongoengineGenericType, AsyncMongoengineGenericObjectTypeOptions + + +AsyncMongoengineObjectType, AsyncMongoengineObjectTypeOptions = create_graphene_generic_class_async(ObjectType, + ObjectTypeOptions) + +AsyncGrapheneMongoengineObjectTypes = (AsyncMongoengineObjectType,) diff --git a/graphene_mongo/utils.py b/graphene_mongo/utils.py index b80b9178..3174a1df 100644 --- a/graphene_mongo/utils.py +++ b/graphene_mongo/utils.py @@ -1,16 +1,21 @@ from __future__ import unicode_literals +import enum import inspect from collections import OrderedDict import mongoengine from graphene import Node from graphene.utils.trim_docstring import trim_docstring -# from graphql.utils.ast_to_dict import ast_to_dict from graphql import FieldNode from graphql_relay.connection.array_connection import offset_to_cursor +class ExecutorEnum(enum.Enum): + ASYNC = enum.auto() + SYNC = enum.auto() + + def get_model_fields(model, excluding=None): excluding = excluding or [] attributes = dict() From 7c99bc12a615df8e4e2822b13bc7035aafae9727 Mon Sep 17 00:00:00 2001 From: Arun Suresh Kumar Date: Wed, 12 Apr 2023 00:18:08 +0530 Subject: [PATCH 2/4] Fix: connection_resolver --- graphene_mongo/fields_async.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/graphene_mongo/fields_async.py b/graphene_mongo/fields_async.py index fed0c321..6e59fe65 100644 --- a/graphene_mongo/fields_async.py +++ b/graphene_mongo/fields_async.py @@ -296,6 +296,7 @@ async def chained_resolver(self, resolver, is_partial, root, info, **args): return await self.default_resolver(root, info, required_fields, **args) + @classmethod async def connection_resolver(cls, resolver, connection_type, root, info, **args): if root: for key, value in root.__dict__.items(): @@ -308,8 +309,7 @@ async def connection_resolver(cls, resolver, connection_type, root, info, **args if isinstance(connection_type, graphene.NonNull): connection_type = connection_type.of_type if Promise.is_thenable(iterable): - # on_resolve = partial(cls.resolve_connection, connection_type, args) - iterable = Promise.resolve(iterable).value - + on_resolve = partial(cls.resolve_connection, connection_type, args) + iterable = Promise.resolve(iterable).then(on_resolve).value return await sync_to_async(cls.resolve_connection, thread_sensitive=False, executor=ThreadPoolExecutor())(connection_type, args, iterable) From 2fe50088d1126c4d34170a4ab1664a2adef985ef Mon Sep 17 00:00:00 2001 From: Arun Suresh Kumar Date: Wed, 12 Apr 2023 02:22:05 +0530 Subject: [PATCH 3/4] Support Async, Test Case Added --- graphene_mongo/converter.py | 52 +++++++----- graphene_mongo/fields_async.py | 6 -- graphene_mongo/registry.py | 19 ++++- graphene_mongo/tests/nodes.py | 19 ++--- graphene_mongo/tests/nodes_async.py | 45 ++++++----- graphene_mongo/tests/test_relay_query.py | 1 - .../tests/test_relay_query_async.py | 81 +++++++++---------- 7 files changed, 119 insertions(+), 104 deletions(-) diff --git a/graphene_mongo/converter.py b/graphene_mongo/converter.py index d4ae4856..47c0c78a 100644 --- a/graphene_mongo/converter.py +++ b/graphene_mongo/converter.py @@ -152,7 +152,10 @@ def reference_resolver(root, *args, **kwargs): querying_union_types.remove('__typename') to_resolve_models = list() for each in querying_union_types: - to_resolve_models.append(registry._registry_string_map[each]) + if executor == ExecutorEnum.SYNC: + to_resolve_models.append(registry._registry_string_map[each]) + else: + to_resolve_models.append(registry._registry_async_string_map[each]) to_resolve_object_ids = list() for each in to_resolve: if isinstance(each, LazyReference): @@ -219,7 +222,10 @@ async def reference_resolver_async(root, *args, **kwargs): querying_union_types.remove('__typename') to_resolve_models = list() for each in querying_union_types: - to_resolve_models.append(registry._registry_string_map[each]) + if executor == ExecutorEnum.SYNC: + to_resolve_models.append(registry._registry_string_map[each]) + else: + to_resolve_models.append(registry._registry_async_string_map[each]) to_resolve_object_ids = list() for each in to_resolve: if isinstance(each, LazyReference): @@ -297,7 +303,7 @@ def convert_field_to_union(field, registry=None, executor: ExecutorEnum = Execut elif isinstance(field, mongoengine.GenericEmbeddedDocumentField): _field = mongoengine.EmbeddedDocumentField(choice) - _field = convert_mongoengine_field(_field, registry) + _field = convert_mongoengine_field(_field, registry, executor=executor) _type = _field.get_type() if _type: _types.append(_type.type) @@ -311,7 +317,7 @@ def convert_field_to_union(field, registry=None, executor: ExecutorEnum = Execut name = to_camel_case("{}_{}".format( field._owner_document.__name__, field.db_field - )) + "UnionType" + )) + "UnionType" if ExecutorEnum.SYNC else "AsyncUnionType" Meta = type("Meta", (object,), {"types": tuple(_types)}) _union = type(name, (graphene.Union,), {"Meta": Meta}) @@ -320,7 +326,7 @@ def reference_resolver(root, *args, **kwargs): if de_referenced: document = get_document(de_referenced["_cls"]) document_field = mongoengine.ReferenceField(document) - document_field = convert_mongoengine_field(document_field, registry) + document_field = convert_mongoengine_field(document_field, registry, executor=executor) _type = document_field.get_type().type filter_args = list() if _type._meta.filter_fields: @@ -344,7 +350,7 @@ def lazy_reference_resolver(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - document_field_type = registry.get_type_for_model(document.document_type) + document_field_type = registry.get_type_for_model(document.document_type, executor=executor) querying_types = list(get_query_fields(args[0]).keys()) filter_args = list() if document_field_type._meta.filter_fields: @@ -356,7 +362,7 @@ def lazy_reference_resolver(root, *args, **kwargs): item = to_snake_case(each) if item in document.document_type._fields_ordered + tuple(filter_args): queried_fields.append(item) - _type = registry.get_type_for_model(document.document_type) + _type = registry.get_type_for_model(document.document_type, executor=executor) return document.document_type.objects().no_dereference().only( *(set((list(_type._meta.required_fields) + queried_fields)))).get( pk=document.pk) @@ -393,7 +399,7 @@ async def lazy_reference_resolver_async(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - document_field_type = registry.get_type_for_model(document.document_type) + document_field_type = registry.get_type_for_model(document.document_type, executor=executor) querying_types = list(get_query_fields(args[0]).keys()) filter_args = list() if document_field_type._meta.filter_fields: @@ -405,7 +411,7 @@ async def lazy_reference_resolver_async(root, *args, **kwargs): item = to_snake_case(each) if item in document.document_type._fields_ordered + tuple(filter_args): queried_fields.append(item) - _type = registry.get_type_for_model(document.document_type) + _type = registry.get_type_for_model(document.document_type, executor=executor) return await sync_to_async(document.document_type.objects().no_dereference().only( *(set((list(_type._meta.required_fields) + queried_fields)))).get, thread_sensitive=False, executor=ThreadPoolExecutor())(pk=document.pk) @@ -418,7 +424,8 @@ async def lazy_reference_resolver_async(root, *args, **kwargs): required = False if field.db_field is not None: required = field.required - resolver_function = getattr(registry.get_type_for_model(field.owner_document), "resolve_" + field.db_field, + resolver_function = getattr(registry.get_type_for_model(field.owner_document, executor=executor), + "resolve_" + field.db_field, None) if resolver_function and callable(resolver_function): field_resolver = resolver_function @@ -431,7 +438,8 @@ async def lazy_reference_resolver_async(root, *args, **kwargs): required = False if field.db_field is not None: required = field.required - resolver_function = getattr(registry.get_type_for_model(field.owner_document), "resolve_" + field.db_field, + resolver_function = getattr(registry.get_type_for_model(field.owner_document, executor=executor), + "resolve_" + field.db_field, None) if resolver_function and callable(resolver_function): field_resolver = resolver_function @@ -452,7 +460,7 @@ def reference_resolver(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - _type = registry.get_type_for_model(field.document_type) + _type = registry.get_type_for_model(field.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -470,7 +478,7 @@ def reference_resolver(root, *args, **kwargs): def cached_reference_resolver(root, *args, **kwargs): if field: queried_fields = list() - _type = registry.get_type_for_model(field.document_type) + _type = registry.get_type_for_model(field.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -490,7 +498,7 @@ async def reference_resolver_async(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - _type = registry.get_type_for_model(field.document_type) + _type = registry.get_type_for_model(field.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -508,7 +516,7 @@ async def reference_resolver_async(root, *args, **kwargs): async def cached_reference_resolver_async(root, *args, **kwargs): if field: queried_fields = list() - _type = registry.get_type_for_model(field.document_type) + _type = registry.get_type_for_model(field.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -526,7 +534,7 @@ async def cached_reference_resolver_async(root, *args, **kwargs): return None def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_type_for_model(model, executor=executor) if not _type: return None if isinstance(field, mongoengine.EmbeddedDocumentField): @@ -536,7 +544,8 @@ def dynamic_type(): required = False if field.db_field is not None: required = field.required - resolver_function = getattr(registry.get_type_for_model(field.owner_document), "resolve_" + field.db_field, + resolver_function = getattr(registry.get_type_for_model(field.owner_document, executor=executor), + "resolve_" + field.db_field, None) if resolver_function and callable(resolver_function): field_resolver = resolver_function @@ -560,7 +569,7 @@ def lazy_resolver(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - _type = registry.get_type_for_model(document.document_type) + _type = registry.get_type_for_model(document.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -579,7 +588,7 @@ async def lazy_resolver_async(root, *args, **kwargs): document = getattr(root, field.name or field.db_name) if document: queried_fields = list() - _type = registry.get_type_for_model(document.document_type) + _type = registry.get_type_for_model(document.document_type, executor=executor) filter_args = list() if _type._meta.filter_fields: for key, values in _type._meta.filter_fields.items(): @@ -595,14 +604,15 @@ async def lazy_resolver_async(root, *args, **kwargs): return None def dynamic_type(): - _type = registry.get_type_for_model(model) + _type = registry.get_type_for_model(model, executor=executor) if not _type: return None field_resolver = None required = False if field.db_field is not None: required = field.required - resolver_function = getattr(registry.get_type_for_model(field.owner_document), "resolve_" + field.db_field, + resolver_function = getattr(registry.get_type_for_model(field.owner_document, executor=executor), + "resolve_" + field.db_field, None) if resolver_function and callable(resolver_function): field_resolver = resolver_function diff --git a/graphene_mongo/fields_async.py b/graphene_mongo/fields_async.py index 6e59fe65..bf3daafd 100644 --- a/graphene_mongo/fields_async.py +++ b/graphene_mongo/fields_async.py @@ -34,12 +34,6 @@ class AsyncMongoengineConnectionField(MongoengineConnectionField): def __init__(self, type, *args, **kwargs): - get_queryset = kwargs.pop("get_queryset", None) - if get_queryset: - assert callable( - get_queryset - ), "Attribute `get_queryset` on {} must be callable.".format(self) - self._get_queryset = get_queryset super(AsyncMongoengineConnectionField, self).__init__(type, *args, **kwargs) @property diff --git a/graphene_mongo/registry.py b/graphene_mongo/registry.py index a7aa61d3..feea4d0a 100644 --- a/graphene_mongo/registry.py +++ b/graphene_mongo/registry.py @@ -1,10 +1,14 @@ from graphene import Enum +from graphene_mongo.utils import ExecutorEnum + class Registry(object): def __init__(self): self._registry = {} + self._registry_async = {} self._registry_string_map = {} + self._registry_async_string_map = {} self._registry_enum = {} def register(self, cls): @@ -21,8 +25,12 @@ def register(self, cls): cls.__name__ ) assert cls._meta.registry == self, "Registry for a Model have to match." - self._registry[cls._meta.model] = cls - self._registry_string_map[cls.__name__] = cls._meta.model.__name__ + if issubclass(cls, GrapheneMongoengineObjectTypes): + self._registry[cls._meta.model] = cls + self._registry_string_map[cls.__name__] = cls._meta.model.__name__ + else: + self._registry_async[cls._meta.model] = cls + self._registry_async_string_map[cls.__name__] = cls._meta.model.__name__ # Rescan all fields for model, cls in self._registry.items(): @@ -40,8 +48,11 @@ def register_enum(self, cls): cls.__name__ = name self._registry_enum[cls] = Enum.from_enum(cls) - def get_type_for_model(self, model): - return self._registry.get(model) + def get_type_for_model(self, model, executor: ExecutorEnum = ExecutorEnum.SYNC): + if executor == ExecutorEnum.SYNC: + return self._registry.get(model) + else: + return self._registry_async.get(model) def check_enum_already_exist(self, cls): return cls in self._registry_enum diff --git a/graphene_mongo/tests/nodes.py b/graphene_mongo/tests/nodes.py index 3053f300..ef98cad1 100644 --- a/graphene_mongo/tests/nodes.py +++ b/graphene_mongo/tests/nodes.py @@ -3,6 +3,7 @@ from . import models from . import types # noqa: F401 +from .models import ProfessorMetadata from ..types_async import AsyncMongoengineObjectType from ..types import MongoengineObjectType @@ -48,12 +49,6 @@ class Meta: interfaces = (Node,) -class ReporterNodeAsync(AsyncMongoengineObjectType): - class Meta: - model = models.Reporter - interfaces = (Node,) - - class ParentNode(MongoengineObjectType): class Meta: model = models.Parent @@ -72,16 +67,22 @@ class Meta: interfaces = (Node,) +class ChildRegisteredAfterNode(MongoengineObjectType): + class Meta: + model = models.ChildRegisteredAfter + interfaces = (Node,) + + class ParentWithRelationshipNode(MongoengineObjectType): class Meta: model = models.ParentWithRelationship interfaces = (Node,) -class ChildRegisteredAfterNode(MongoengineObjectType): +class ProfessorMetadataNode(MongoengineObjectType): class Meta: - model = models.ChildRegisteredAfter - interfaces = (Node,) + model = ProfessorMetadata + interfaces = (graphene.Node,) class ProfessorVectorNode(MongoengineObjectType): diff --git a/graphene_mongo/tests/nodes_async.py b/graphene_mongo/tests/nodes_async.py index ac7fb95e..a38635ce 100644 --- a/graphene_mongo/tests/nodes_async.py +++ b/graphene_mongo/tests/nodes_async.py @@ -3,10 +3,11 @@ from . import models from . import types # noqa: F401 +from .models import ProfessorMetadata from ..types_async import AsyncMongoengineObjectType -class PublisherNode(AsyncMongoengineObjectType): +class PublisherAsyncNode(AsyncMongoengineObjectType): legal_name = graphene.String() bad_field = graphene.String() @@ -16,92 +17,92 @@ class Meta: interfaces = (Node,) -class ArticleNode(AsyncMongoengineObjectType): +class ArticleAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Article interfaces = (Node,) -class EditorNode(AsyncMongoengineObjectType): +class EditorAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Editor interfaces = (Node,) -class EmbeddedArticleNode(AsyncMongoengineObjectType): +class EmbeddedArticleAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.EmbeddedArticle interfaces = (Node,) -class PlayerNode(AsyncMongoengineObjectType): +class PlayerAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Player interfaces = (Node,) filter_fields = {"first_name": ["istartswith", "in"]} -class ReporterNode(AsyncMongoengineObjectType): +class ReporterAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Reporter interfaces = (Node,) -class ReporterNodeAsync(AsyncMongoengineObjectType): +class ParentAsyncNode(AsyncMongoengineObjectType): class Meta: - model = models.Reporter + model = models.Parent interfaces = (Node,) -class ParentNode(AsyncMongoengineObjectType): +class ChildAsyncNode(AsyncMongoengineObjectType): class Meta: - model = models.Parent + model = models.Child interfaces = (Node,) -class ChildNode(AsyncMongoengineObjectType): +class ChildRegisteredBeforeAsyncNode(AsyncMongoengineObjectType): class Meta: - model = models.Child + model = models.ChildRegisteredBefore interfaces = (Node,) -class ChildRegisteredBeforeNode(AsyncMongoengineObjectType): +class ChildRegisteredAfterAsyncNode(AsyncMongoengineObjectType): class Meta: - model = models.ChildRegisteredBefore + model = models.ChildRegisteredAfter interfaces = (Node,) -class ParentWithRelationshipNode(AsyncMongoengineObjectType): +class ParentWithRelationshipAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.ParentWithRelationship interfaces = (Node,) -class ChildRegisteredAfterNode(AsyncMongoengineObjectType): +class ProfessorMetadataAsyncNode(AsyncMongoengineObjectType): class Meta: - model = models.ChildRegisteredAfter - interfaces = (Node,) + model = ProfessorMetadata + interfaces = (graphene.Node,) -class ProfessorVectorNode(AsyncMongoengineObjectType): +class ProfessorVectorAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.ProfessorVector interfaces = (Node,) -class ErroneousModelNode(AsyncMongoengineObjectType): +class ErroneousModelAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.ErroneousModel interfaces = (Node,) -class BarNode(AsyncMongoengineObjectType): +class BarAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Bar interfaces = (Node,) -class FooNode(AsyncMongoengineObjectType): +class FooAsyncNode(AsyncMongoengineObjectType): class Meta: model = models.Foo interfaces = (Node,) diff --git a/graphene_mongo/tests/test_relay_query.py b/graphene_mongo/tests/test_relay_query.py index 9824477a..1e2d7ba3 100644 --- a/graphene_mongo/tests/test_relay_query.py +++ b/graphene_mongo/tests/test_relay_query.py @@ -9,7 +9,6 @@ from . import models from . import nodes -from .. import AsyncMongoengineConnectionField from ..fields import MongoengineConnectionField from ..types import MongoengineObjectType diff --git a/graphene_mongo/tests/test_relay_query_async.py b/graphene_mongo/tests/test_relay_query_async.py index 71c66b34..83b62ba4 100644 --- a/graphene_mongo/tests/test_relay_query_async.py +++ b/graphene_mongo/tests/test_relay_query_async.py @@ -10,14 +10,12 @@ from . import models from . import nodes_async from .. import AsyncMongoengineConnectionField, AsyncMongoengineObjectType -from ..fields import MongoengineConnectionField -from ..types import MongoengineObjectType @pytest.mark.asyncio async def test_should_query_reporter_async(fixtures): class Query(graphene.ObjectType): - reporter = graphene.Field(nodes_async.ReporterNode) + reporter = graphene.Field(nodes_async.ReporterAsyncNode) async def resolve_reporter(self, *args, **kwargs): return models.Reporter.objects.no_dereference().first() @@ -52,7 +50,7 @@ async def resolve_reporter(self, *args, **kwargs): }, genericReference { __typename - ... on ArticleNode { + ... on ArticleAsyncNode { headline } } @@ -83,7 +81,7 @@ async def resolve_reporter(self, *args, **kwargs): {"node": {"headline": "Real"}}, ] }, - "genericReference": {"__typename": "ArticleNode", "headline": "Hello"}, + "genericReference": {"__typename": "ArticleAsyncNode", "headline": "Hello"}, } } @@ -96,7 +94,7 @@ async def resolve_reporter(self, *args, **kwargs): @pytest.mark.asyncio async def test_should_query_reporters_with_nested_document_async(fixtures): class Query(graphene.ObjectType): - reporters = AsyncMongoengineConnectionField(nodes_async.ReporterNode) + reporters = AsyncMongoengineConnectionField(nodes_async.ReporterAsyncNode) query = """ query ReporterQuery { @@ -142,7 +140,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_query_all_editors_async(fixtures, fixtures_dirname): class Query(graphene.ObjectType): - editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + editors = AsyncMongoengineConnectionField(nodes_async.EditorAsyncNode) query = """ query EditorQuery { @@ -172,7 +170,7 @@ class Query(graphene.ObjectType): "edges": [ { "node": { - "id": "RWRpdG9yTm9kZTox", + "id": "RWRpdG9yQXN5bmNOb2RlOjE=", "firstName": "Penny", "lastName": "Hardaway", "avatar": { @@ -184,7 +182,7 @@ class Query(graphene.ObjectType): }, { "node": { - "id": "RWRpdG9yTm9kZToy", + "id": "RWRpdG9yQXN5bmNOb2RlOjI=", "firstName": "Grant", "lastName": "Hill", "avatar": {"contentType": None, "length": 0, "data": None}, @@ -192,7 +190,7 @@ class Query(graphene.ObjectType): }, { "node": { - "id": "RWRpdG9yTm9kZToz", + "id": "RWRpdG9yQXN5bmNOb2RlOjM=", "firstName": "Dennis", "lastName": "Rodman", "avatar": {"contentType": None, "length": 0, "data": None}, @@ -213,7 +211,7 @@ async def test_should_query_editors_with_dataloader_async(fixtures): from promise.dataloader import DataLoader class ArticleLoader(DataLoader): - async def batch_load_fn(self, instances): + def batch_load_fn(self, instances): queryset = models.Article.objects(editor__in=instances) return Promise.resolve( [ @@ -229,7 +227,7 @@ class Meta: model = models.Editor interfaces = (graphene.Node,) - articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + articles = AsyncMongoengineConnectionField(nodes_async.ArticleAsyncNode) async def resolve_articles(self, *args, **kwargs): return article_loader.load(self) @@ -277,11 +275,11 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_filter_editors_by_id_async(fixtures): class Query(graphene.ObjectType): - editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + editors = AsyncMongoengineConnectionField(nodes_async.EditorAsyncNode) query = """ query EditorQuery { - editors(id: "RWRpdG9yTm9kZToy") { + editors(id: "RWRpdG9yQXN5bmNOb2RlOjI=") { edges { node { id, @@ -297,7 +295,7 @@ class Query(graphene.ObjectType): "edges": [ { "node": { - "id": "RWRpdG9yTm9kZToy", + "id": "RWRpdG9yQXN5bmNOb2RlOjI=", "firstName": "Grant", "lastName": "Hill", } @@ -314,7 +312,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_filter_async(fixtures): class Query(graphene.ObjectType): - articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + articles = AsyncMongoengineConnectionField(nodes_async.ArticleAsyncNode) query = """ query ArticlesQuery { @@ -353,7 +351,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_filter_by_reference_field_async(fixtures): class Query(graphene.ObjectType): - articles = AsyncMongoengineConnectionField(nodes_async.ArticleNode) + articles = AsyncMongoengineConnectionField(nodes_async.ArticleAsyncNode) query = """ query ArticlesQuery { @@ -384,7 +382,7 @@ class Query(graphene.ObjectType): async def test_should_filter_through_inheritance_async(fixtures): class Query(graphene.ObjectType): node = Node.Field() - children = AsyncMongoengineConnectionField(nodes_async.ChildNode) + children = AsyncMongoengineConnectionField(nodes_async.ChildAsyncNode) query = """ query ChildrenQuery { @@ -425,7 +423,7 @@ class Query(graphene.ObjectType): async def test_should_filter_by_list_contains_async(fixtures): # Notes: https://goo.gl/hMNRgs class Query(graphene.ObjectType): - reporters = AsyncMongoengineConnectionField(nodes_async.ReporterNodeAsync) + reporters = AsyncMongoengineConnectionField(nodes_async.ReporterAsyncNode) query = """ query ReportersQuery { @@ -437,7 +435,7 @@ class Query(graphene.ObjectType): awards, genericReferences { __typename - ... on ArticleNode { + ... on ArticleAsyncNode { headline } } @@ -451,12 +449,12 @@ class Query(graphene.ObjectType): "edges": [ { "node": { - "id": "UmVwb3J0ZXJOb2RlQXN5bmM6MQ==", + "id": "UmVwb3J0ZXJBc3luY05vZGU6MQ==", "firstName": "Allen", "awards": ["2010-mvp"], "genericReferences": [ { - "__typename": "ArticleNode", + "__typename": "ArticleAsyncNode", "headline": "Hello" } ] @@ -475,11 +473,11 @@ class Query(graphene.ObjectType): async def test_should_filter_by_id_async(fixtures): # Notes: https://goo.gl/hMNRgs class Query(graphene.ObjectType): - reporter = Node.Field(nodes_async.ReporterNode) + reporter = Node.Field(nodes_async.ReporterAsyncNode) query = """ query ReporterQuery { - reporter (id: "UmVwb3J0ZXJOb2RlOjE=") { + reporter (id: "UmVwb3J0ZXJBc3luY05vZGU6MQ==") { id, firstName, awards @@ -488,7 +486,7 @@ class Query(graphene.ObjectType): """ expected = { "reporter": { - "id": "UmVwb3J0ZXJOb2RlOjE=", + "id": "UmVwb3J0ZXJBc3luY05vZGU6MQ==", "firstName": "Allen", "awards": ["2010-mvp"], } @@ -502,7 +500,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_first_n_async(fixtures): class Query(graphene.ObjectType): - editors = AsyncMongoengineConnectionField(nodes_async.EditorNode) + editors = AsyncMongoengineConnectionField(nodes_async.EditorAsyncNode) query = """ query EditorQuery { @@ -546,7 +544,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_after_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query EditorQuery { @@ -579,7 +577,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_before_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query EditorQuery { @@ -614,7 +612,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_last_n_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query PlayerQuery { @@ -646,7 +644,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_self_reference_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query PlayersQuery { @@ -722,9 +720,10 @@ class Query(graphene.ObjectType): async def test_should_lazy_reference_async(fixtures): class Query(graphene.ObjectType): node = Node.Field() - parents = AsyncMongoengineConnectionField(nodes_async.ParentWithRelationshipNode) + parents = AsyncMongoengineConnectionField(nodes_async.ParentWithRelationshipAsyncNode) schema = graphene.Schema(query=Query) + print(schema) query = """ query { @@ -782,7 +781,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_query_with_embedded_document_async(fixtures): class Query(graphene.ObjectType): - professors = AsyncMongoengineConnectionField(nodes_async.ProfessorVectorNode) + professors = AsyncMongoengineConnectionField(nodes_async.ProfessorVectorAsyncNode) query = """ query { @@ -816,7 +815,7 @@ async def test_should_get_queryset_returns_dict_filters_async(fixtures): class Query(graphene.ObjectType): node = Node.Field() articles = AsyncMongoengineConnectionField( - nodes_async.ArticleNode, get_queryset=lambda *_, **__: {"headline": "World"} + nodes_async.ArticleAsyncNode, get_queryset=lambda *_, **__: {"headline": "World"} ) query = """ @@ -855,13 +854,13 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_get_queryset_returns_qs_filters_async(fixtures): - async def get_queryset(model, info, **args): + def get_queryset(model, info, **args): return model.objects(headline="World") class Query(graphene.ObjectType): node = Node.Field() articles = AsyncMongoengineConnectionField( - nodes_async.ArticleNode, get_queryset=get_queryset + nodes_async.ArticleAsyncNode, get_queryset=get_queryset ) query = """ @@ -901,7 +900,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_filter_mongoengine_queryset_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query players { @@ -934,7 +933,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_query_document_with_embedded_async(fixtures): class Query(graphene.ObjectType): - foos = AsyncMongoengineConnectionField(nodes_async.FooNode) + foos = AsyncMongoengineConnectionField(nodes_async.FooAsyncNode) async def resolve_multiple_foos(self, *args, **kwargs): return list(models.Foo.objects.all()) @@ -965,7 +964,7 @@ async def resolve_multiple_foos(self, *args, **kwargs): @pytest.mark.asyncio async def test_should_filter_mongoengine_queryset_with_list_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query players { @@ -998,7 +997,7 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_get_correct_list_of_documents_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) query = """ query players { @@ -1048,10 +1047,10 @@ class Query(graphene.ObjectType): @pytest.mark.asyncio async def test_should_filter_mongoengine_queryset_by_id_and_other_fields_async(fixtures): class Query(graphene.ObjectType): - players = AsyncMongoengineConnectionField(nodes_async.PlayerNode) + players = AsyncMongoengineConnectionField(nodes_async.PlayerAsyncNode) larry = models.Player.objects.get(first_name="Larry") - larry_relay_id = to_global_id("PlayerNode", larry.id) + larry_relay_id = to_global_id("PlayerAsyncNode", larry.id) # "Larry" id && firstName == "Michael" should return nothing query = """ From 9581ad2767e1f4a894e6a9c0a1a0e4d33b8859b3 Mon Sep 17 00:00:00 2001 From: Arun Suresh Kumar Date: Wed, 12 Apr 2023 02:22:44 +0530 Subject: [PATCH 4/4] Bump Version : 0.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8c925d6d..9ae880d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "graphene-mongo" packages = [{ include = "graphene_mongo" }] -version = "0.3.0" +version = "0.4.0" description = "Graphene Mongoengine integration" authors = [ "Abaw Chen ",