graphql-python / graphene-mongo

Graphene MongoEngine integration
http://graphene-mongo.readthedocs.io/en/latest/
MIT License
288 stars 113 forks source link

[BUG] Document subclasses are cast to parent class in query results #93

Closed Cerebus closed 3 years ago

Cerebus commented 5 years ago

I know I'm missing something. Given a model with multiple document types in a collection:

class AModel(Document):
    meta = {'allow_inheritance': True}
    a_field = StringField()

class ASubModel(AModel):
    a_sub_field = StringField()

I can construct a schema and query:

class A(MongoengineObjectType):                                           
    class Meta:                                                                
        model = AModel                                                    
        interfaces = (Node,)                                                   

class ASub(A):                                                      
    class Meta:                                                                
        model = ASubModel                                                
        interfaces = (Node,)

class Query(graphene.ObjectType):                                              
    node = Node.Field()                                                        
    all_a = MongoengineConnectionField(A)                           

If I run the allA query, I get back all the documents as expected, but I can't access ASub's fields because the returned type is A, not ASub, even though the class is identified as A.ASub. E.g.,:

{
  "data": {
    "allA": {
      "edges": [
        {
          "node": {
            "id": "VGFyZ2V0OjVjZTVmOTMwMjRmN2NhMGJmMjZlNzZmMQ==",
            "Cls": "A.ASub",
            "__typename": "A"
          }
        }
     ]
  }
}

Attempting to resolve ASub.a_sub_field results in an error. Inline fragment doesn't work either:

{
  "errors": [
    {
      "message": "Fragment cannot be spread here as objects of type A can never be of type ASub",
      "locations": [
        {
          "line": 8,
          "column": 9
        }
      ]
    }
  ]
}

I'm clearly doing something wrong, but I don't know what.

Cerebus commented 5 years ago

Using the examples above, mongoengine returns the subclass when fetching on the parent class, which is IMHO correct behavior:

>>> AModel.objects.all()
[<ASubModel: ASubModel object>]

However, MongoengineConnectionField descends from graphene.Field, which adopts the named class as the only possible type. As a result, when the query is run returned results are being cast to MongoengineConnectionField._type, which is the parent class in this example. The casting works because of inheritance, but it's unexpected behavior when you're using a mongo backend.

I don't think the other graphene backends won't have this issue.

IMHO this is a bug. Document subclassing is a key mongo feature, so graphene_mongo should support it. I think the right way is to override graphene's behavior here and set __typename to the returned object's class, with an assertion that the returned object is a subclass of MongoengineConnectionField._type.

I think this would allow inline fragments to access fields on each subclass in a way compatible with mongo.

abawchen commented 5 years ago

@Cerebus Regarding your Query class

class Query(graphene.ObjectType):                                              
    node = Node.Field()                                                        
    all_a = MongoengineConnectionField(A)

should be

class Query(graphene.ObjectType):                                              
    node = Node.Field()                                                        
    all_a = MongoengineConnectionField(ASub)

^ Make sense?

Cerebus commented 5 years ago

Yes, but...if I add a new Document subclass,

class ASub_new(A):
    pass

Then I have a different problem.

I could use a union, but even that makes me unhappy since I still have to modify the Query class every time I extend the data model. Further, it seems like it takes me away from Relay's interface, which I'd rather not do.

abawchen commented 4 years ago

@Cerebus : after doing some research, I found this as the workaround(?) by adding field and it's resolve method, and yes, there is another effort (adding fields) though ... :(, but I think it's hard to know what fields child-model added automatically.

Please let me know if any thought, thanks.

import graphene
import mongoengine
from datetime import datetime
from graphene_mongo import (
    MongoengineObjectType,
    MongoengineConnectionField
)

mongoengine.connect(
    "graphene-mongo-test", host="mongomock://localhost", alias="default"
)

class AModel(mongoengine.Document):
    meta = {'allow_inheritance': True}
    a_field = mongoengine.StringField()

class ASubModel(AModel):
    a_sub_field = mongoengine.StringField()

class A(MongoengineObjectType):
    a_sub_field = graphene.String()

    class Meta:
        model = AModel
        interfaces = (graphene.Node,)

class ASub(A):
    class Meta:
        model = ASubModel
        interfaces = (graphene.Node,)

AModel.drop_collection()
a_model = AModel(a_field='a_model_1')
a_model.save()
a_sub_model = ASubModel(a_field='a_field_abc', a_sub_field='hello')
a_sub_model.save()

class Query(graphene.ObjectType):
    all_a = MongoengineConnectionField(A)

query = """
    query Query {
        allA {
            edges {
                node {
                    aField,
                    aSubField,
                }
            }
        }
    }
"""

schema = graphene.Schema(query=Query)
result = schema.execute(query)
print(result.data)
abawchen commented 3 years ago

close due to stale, feel free to re-open it.