Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for default mutations #30

Open
KCarretto opened this issue May 2, 2018 · 6 comments
Open

Support for default mutations #30

KCarretto opened this issue May 2, 2018 · 6 comments
Assignees
Labels
good first issue Good for newcomers help wanted Extra attention is needed

Comments

@KCarretto
Copy link

Are there any plans to add support for simple create / update / delete mutations? It seems like you could view the fields associated with a Mongoengine Document and automatically build the arguments to a graphene mutation, instead of needing to manually write one for each object. Thoughts on this?

@KCarretto
Copy link
Author

I was able to achieve this functionality using code such as:

class Arguments:
    test = graphene.String(required=True)

def get_create(object_model, arguments):
    class CreateMutation(graphene.Mutation):
        Arguments = arguments

        model = graphene.Field(object_model)

        def mutate(self, _, **kwargs):
            model = object_model._meta.model(**kwargs)
            model.save(force_insert=True)
            return CreateMutation(model=model)

    return CreateMutation

Although currently you still must explicitly define arguments. If this feature is desired I can submit a PR :)

@KCarretto
Copy link
Author

Update: I ended up needing to return type instead, otherwise it was treated as the same object each time. Here's the new code that works (so far) with more than one model:

def get_create(object_model, arguments):

    class CreateMutation(graphene.Mutation):
        Arguments = arguments

        model = graphene.Field(object_model)

        def mutate(self, _, **kwargs):
            model = object_model._meta.model(**kwargs)
            model.save(force_insert=True)
            return CreateMutation(model=model)

    return type(
        "create{}".format(object_model.__name__),
        (CreateMutation,),
        {
            "Arguments": arguments,
            "model": graphene.Field(object_model)
        }
    )

@abawchen abawchen added help wanted Extra attention is needed good first issue Good for newcomers labels May 3, 2018
@abawchen
Copy link
Collaborator

abawchen commented May 3, 2018

@KCarretto : Thanks for the input! It's a good feature 👍 , and I assign this one to you 😛

Btw, please add the corresponding test case to make sure it works.

@abawchen abawchen assigned abawchen and KCarretto and unassigned abawchen May 3, 2018
@abawchen abawchen added this to To do in Graphene-Mongo May 10, 2018
@zrlay
Copy link
Contributor

zrlay commented May 15, 2018

I've got some working code that overlaps this issue but don't have time this month to publish it! @KCarretto, if you are diving into this, I have some insights you might consider:

For the case of embedded documents, an input registry is extremely useful (also might want to consider using input objects instead of direct kwargs). To automatically generate an InputObject, we're using:

def _create_input_factory(model, exclude_fields=('id', 'idempotency_key'), only_fields=()):
    return type(
        "Create{}Input".format(model.__name__),
        (MongoengineInputObjectType,),
        {"Meta": type('Meta', (), dict(model=model, exclude_fields=exclude_fields, only_fields=only_fields))}
    )

def generate_create_mutation(resolvable, only_fields=(), exclude_fields=()):
    model = get_model_from_resolvable(resolvable)
    assert hasattr(model, "idempotency_key")

    if not only_fields and not exclude_fields:
        only_fields = get_fields_from_resolvable(resolvable)
    CreateInput = _create_input_factory(model, only_fields=only_fields, exclude_fields=exclude_fields)

    CreateMutation = type(
        "Create{}".format(model.__name__),
        (graphene.Mutation,),
        {
            "mutate": partial(create_mutate, resolvable=resolvable, klass=lambda: CreateMutation),
            "success": graphene.Boolean(required=True),
            "{}".format(model.__name__.lower()): graphene.Field(resolvable),
            "Arguments": type(
                "Arguments",
                (),
                dict(
                    idempotency_key=graphene.ID(),
                    input=CreateInput(required=True)
                ))
        }
    )

    get_global_mutation_registry().register(CreateMutation, resolvable)
    return CreateMutation

We're creating a custom name for the mutation so the return type looks nicer in graphiql/docs, but creating a Payload object might be more broadly recognized.

Update and delete are very similar. The api for using this in a schema is:

    create_mytype = generate_create_mutation(Mytype, only_fields=('myattr', 'myotherattr')).Field(description="Create a Mytype record.")

This creates a mutation and input type that looks like createMytype(input: CreateMytypeInput, idempotencyKey: ID): CreateMytype

If we had an embedded document that we wanted to accept as a nested input structure, we would manually create the input type the same way we manually create an embedded objecttype:

class MyNested(MongoengineObjectType):
    class Meta:
        model = models.MyNested

class MyNestedInput(MongoengineInputObjectType):
    class Meta:
        model = models.MyNested

And this would get registered and automatically used in the mutation based on the embedded mongoengine field type.

MongoengineInputObjectType is a fork/copypasta of MongoengineObjectType to use an InputRegistry instead. Also we created a version of converter.py which contains convert_mongoengine_input_field because required=True is different than null=False, especially for Boolean fields.

from collections import OrderedDict
from graphene import InputField
from graphene.types.inputobjecttype import InputObjectType, InputObjectTypeOptions, InputObjectTypeContainer
from graphene.types.utils import yank_fields_from_attrs
from graphene_mongo.types import construct_self_referenced_fields
from graphene_mongo.utils import is_valid_mongoengine_model, get_model_fields
from mongoengine import ListField

def construct_fields(model, registry, only_fields, exclude_fields):
    _model_fields = get_model_fields(model)
    fields = OrderedDict()
    self_referenced = OrderedDict()
    for name, field in _model_fields.items():
        is_not_in_only = only_fields and name not in only_fields
        is_excluded = name in exclude_fields
        if is_not_in_only or is_excluded:
            # We skip this field if we specify only_fields and is not
            # in there. Or when we exclude this field in exclude_fields
            continue
        if isinstance(field, ListField):
            # Take care of list of self-reference.
            document_type_obj = field.field.__dict__.get('document_type_obj', None)
            if document_type_obj == model._class_name \
                or isinstance(document_type_obj, model) \
                or document_type_obj == model:
                self_referenced[name] = field
                continue
        converted = convert_mongoengine_input_field(field, registry)
        if not converted:
            continue
        fields[name] = converted

    return fields, self_referenced


class MongoengineInputObjectTypeOptions(InputObjectTypeOptions):
    model = None
    registry = None  # type: InputRegistry


class MongoengineInputObjectTypeContainer(InputObjectTypeContainer):

    def __init__(self, *args, **kwargs):
        dict.__init__(self, *args, **kwargs)
        for key in self._meta.fields.keys():
            setattr(self, key, self.get(key, None))


class MongoengineInputObjectType(InputObjectType):

    @classmethod
    def __init_subclass_with_meta__(cls, model=None, input_registry=None, skip_input_registry=False,
                                    only_fields=(), exclude_fields=(), **options):

        assert is_valid_mongoengine_model(model), (
            'You need to pass a valid Mongoengine Model in {}.Meta, received "{}".'
        ).format(cls.__name__, model)

        if not input_registry:
            input_registry = get_global_input_registry()

        assert isinstance(input_registry, InputRegistry), (
            'The attribute input_registry in {} needs to be an instance of '
            'InputRegistry, received "{}".'
        ).format(cls.__name__, input_registry)

        converted_fields, self_referenced = construct_fields(
            model, input_registry, only_fields, exclude_fields
        )
        mongoengine_fields = yank_fields_from_attrs(converted_fields, _as=InputField)

        _meta = MongoengineInputObjectTypeOptions(cls)
        _meta.model = model
        _meta.registry = input_registry
        _meta.fields = mongoengine_fields

        if not _meta:
            _meta = InputObjectTypeOptions(cls)

        fields = OrderedDict()
        for base in reversed(cls.__mro__):
            fields.update(
                yank_fields_from_attrs(base.__dict__, _as=InputField)
            )

        if _meta.fields:
            _meta.fields.update(fields)
        else:
            _meta.fields = fields

        container = type(cls.__name__, (MongoengineInputObjectTypeContainer, cls), mongoengine_fields)
        _meta.container = container

        super(InputObjectType, cls).__init_subclass_with_meta__(
            _meta=_meta, **options)

        if not skip_input_registry:
            input_registry.register(cls)
            # Notes: Take care list of self-reference fields.
            converted_fields = construct_self_referenced_fields(self_referenced, input_registry)
            if converted_fields:
                mongoengine_fields = yank_fields_from_attrs(converted_fields, _as=InputField)
                cls._meta.fields.update(mongoengine_fields)
                input_registry.register(cls)

@abawchen
Copy link
Collaborator

abawchen commented Jun 5, 2018

@KCarretto & @zrlay

I end up creating a pull request by myself, which takes part of you guys suggestion (implemented in mutation.py)

There are 2 things I want to bring up:

  • The Arguments can be auto-generated by model's fields (with only_fields, excluded_fields)
  • I have not take EmbeddedDocument and MongoengineInputObjectType at this moment, because @zrlay's code is a little bit to much for me to digest 😛

Feel free to leave your comments in #36 directly, I am all ears.

@KCarretto
Copy link
Author

@abawchen Thanks for the PR, sorry I couldn't get around to it. I'll try to make time to check it out later this week.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
good first issue Good for newcomers help wanted Extra attention is needed
Projects
No open projects
Graphene-Mongo
  
To do
Development

No branches or pull requests

3 participants