graphql-python/graphene-mongo

update mutation on nested document fails

mpgalaxy opened this issue · 5 comments

Hi, I'm trying to update a nested document including an EmbeddedDocumentListField / EmbeddedDocument with graphene-mongo. Creating a new user via create mutation works perfectly fine, but when I try to update a nested document, it fails with the error
Invalid embedded document instance provided to an EmbeddedDocumentField: ['label']

Here is my code:
models.py:

from mongoengine import Document, EmbeddedDocumentListField, EmbeddedDocument
from mongoengine.fields import StringField


class UserLabel(EmbeddedDocument):
    code = StringField()
    value = StringField()


class User(Document):
    meta = {'collection': 'user'}
    first_name = StringField(required=True)
    last_name = StringField(required=True)
    label = EmbeddedDocumentListField(UserLabel)

app.py:

from flask import Flask
from flask_graphql import GraphQLView
import graphene
from graphene_mongo import MongoengineObjectType
from mongoengine import connect
from models import User as UserModel, UserLabel as UserLabelModel

app = Flask(__name__)

class UserLabel(MongoengineObjectType):
    class Meta:
        model = UserLabelModel


class User(MongoengineObjectType):
    class Meta:
        model = UserModel


class UserLabelInput(graphene.InputObjectType):
    code = graphene.String()
    value = graphene.String()


class UserInput(graphene.InputObjectType):
    id = graphene.String()
    first_name = graphene.String()
    last_name = graphene.String()
    label = graphene.List(UserLabelInput, required=False)


class Query(graphene.ObjectType):
    users = graphene.List(User)

    def resolve_users(self, info):
        return list(UserModel.objects.all())


class createUser(graphene.Mutation):
    user = graphene.Field(User)

    class Arguments:
        user_data = UserInput()

    def mutate(root, info, user_data):
        user = UserModel(
            first_name=user_data.first_name,
            last_name=user_data.last_name,
            label=user_data.label
        )
        user.save()
        return createUser(user=user)


class updateUser(graphene.Mutation):
    user = graphene.Field(User)

    class Arguments:
        user_data = UserInput()

    def mutate(self, info, user_data):
        user = UserModel.objects.get(id=user_data.id)

        if user_data.first_name:
            user.first_name = user_data.first_name
        if user_data.last_name:
            user.last_name = user_data.last_name
        if user_data.label:
            user.label = user_data.label

        user.save()
        return updateUser(user=user)


class Mutation(graphene.ObjectType):
    create_user = createUser.Field()
    update_user = updateUser.Field()


schema = graphene.Schema(query=Query, mutation=Mutation)
app.add_url_rule('/graphql', view_func=GraphQLView.as_view('graphql', schema=schema, graphiql=True))

if __name__ == '__main__':
    app.run(debug=True, port=1234)

Trying to run this update mutation via graphiql :

mutation {
  updateUser(userData: {
    id: "5d6f8bbbe3ec841d93229322",
    firstName: "Peter",
    lastName: "Simpson",
    label: [
      {
        code:"DE",
        value: "Peter Simpson"
      }
    ]
  }) {
    user {
      id
      firstName
      lastName
      label {
        code
        value
      }
    }
  }
}

I get the error:

{
  "errors": [
    {
      "message": "ValidationError (User:5d6f8bbbe3ec841d93229322) (Invalid embedded document instance provided to an EmbeddedDocumentField: ['label'])",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "updateUser"
      ]
    }
  ],
  "data": {
    "updateUser": null
  }
} 

Updating without the nested field works perfectly fine.
How can I resolve this ?

Maybe I didn't find the proper way to do this, but I think normally this library should handle update of nested fields apropriately. One dirty workaround is providing all label data in update values, recreating each item and appending it:

class updateUser(graphene.Mutation):
    user = graphene.Field(User)

    class Arguments:
        user_data = UserInput()

    def mutate(self, info, user_data):
        user = UserModel.objects.get(id=user_data.id)

        if user_data.first_name:
            user.first_name = user_data.first_name
        if user_data.last_name:
            user.last_name = user_data.last_name
        if user_data.label:
            user.label = None
            for label in user_data.label:
                user.label.append(UserLabelModel(label.code, label.value))
        user.save()
        return updateUser(user=user)

I'm not closing this issue, because it is not really solved. Maybe someone will provide another way or there will be an update to the library, handling updates of nested fields correctly. ;)

@mpgalaxy : After investigation, it's more like a MongoEngine limitation, and I found another way to update a document with EmbeddedDocumentField in your case:

def mutate(self, info, user_data):
    # Get original user data in dict type
    user_dict = UserModel.objects.exclude('id').get(id=user_data.id).to_mongo().to_dict()

    # Merge to dict for update
    user_dict.update(user_data)

    # Update user, ref: https://stackoverflow.com/a/52996508/9041712
    user = UserModel(**user_dict)
    user.save()
    return updateUser(user=user)

^ Is this more elegant?

I ran into the same problem.
When using EmbeddedDocument, GraphQL cannot display and use ListField fields.

Hi, I solved this problem.

Solution:

Create the class in the schema.py file:

from graphene_mongo import MongoengineConnectionField, MongoengineObjectType
from graphene.relay import Node
from models import StepsNode

class Steps(MongoengineObjectType):
class Meta:
model = StepsNode
interfaces = (Node,)

models.py

class StepsNode(EmbeddedDocument):
name = StringField()

close this one, and feel free to reopen it if needed.