django-model-mutations 0.1.1
Graphene Django mutations for Django models made easier
License: MIT
Keywords: GRAPHENE,GRAPHENE-DJANGO,GRAPHQL,DJANGO,MODELS,API

# Django Model Mutations

This package adds Mutation classes that make creating graphene mutations with django models easier using Django Rest Framework serializers. It extends graphene Mutation class in a similar way to Django Rest Framework views or original Django views.

It also provides easy way to add permissions checks or ensure logged-in user, as well as easy way to override or add funcionality similar to django forms or rest framework views - such as ```get_queryset()``` or ```save()``` functions.

There is also advanced error reporting from rest framework, that returns non-valid fields and error messages.

Inspired by [Saleor](https://github.com/mirumee/saleor), [graphene-django-extras](https://github.com/eamigo86/graphene-django-extras/tree/master/graphene_django_extras) and [django-rest-framework](https://github.com/encode/django-rest-framework)

## Installation
```
pip install django-model-mutations
```


## Basic Usage
Main classes that this package provides:

| mutations | mixins |
| ------ | ------ |
|CreateModelMutation|LoginRequiredMutationMixin|
|CreateBulkModelMutation|
|UpdateModelMutation|
|UpdateBulkModelMutation|
|DeleteModelMutation|
|DeleteBulkModelMutation|


#### Django usage
Input type (Arguments) is generated from serializer fields  
Return type is retrieved by model from global graphene registry, you just have to import it as in example
```python
from django_model_mutations import mutations, mixins
from your_app.types import UserType  # important to import types to register in global registry
from your_app.serializers import UserSerializer


# Create Mutations
# use mixins.LoginRequiredMutationMixin to ensure only logged-in user can perform this mutation
# MAKE SURE this mixin is FIRST in inheritance order
class UserCreateMutation(mixins.LoginRequiredMutationMixin, mutations.CreateModelMutation):
    class Meta:
        serializer_class = UserSerializer
        # OPTIONAL META FIELDS:
        permissions = ('your_app.user_permission',) # OPTIONAL: specify user permissions
        lookup_field = 'publicId'  # OPTIONAL: specify database lookup column, default is 'id' or 'ids'
        return_field_name = 'myUser' # OPTIONAL: specify return field name, default is model name
        input_field_name = 'myUser' # OPTIONAL: specify input field name, defauls is 'input'
        

class UserBulkCreateMutation(mutations.CreateBulkModelMutation):
    class Meta:
        serializer_class = UserSerializer


# Update Mutations
class UserUpdateMutation(mutations.UpdateModelMutation):
    class Meta:
        serializer_class = UserSerializer

# WARNING: Bulk update DOES NOT USE serializer, due to limitations of rest framework serializer. 
# Instead specify model and argument fields by yourself.
class UserBulkUpdateMutation(mutations.UpdateBulkModelMutation):
    class Arguments:
        is_active = graphene.Boolean()

    class Meta:
        model = User

# Delete Mutations
# delete mutations doesn't use serializers, as there is no need
class UserDeleteMutation(mutations.DeleteModelMutation):
    class Meta:
        model = User

class UserBulkDeleteMutation(mutations.DeleteBulkModelMutation):
    class Meta:
        model = User


# Add to graphene schema as usual
class Mutation(graphene.ObjectType):
    user_create = UserCreateMutation.Field()
    ....

schema = graphene.Schema(mutation=Mutation)
```


#### GraphQl usage
The generated GraphQl schema can be modified with ```Meta``` fields as described above in ```UserCreateMutation```.

By default all mutations have ```errors``` field with ```field``` and ```messages``` that contain validation errors from rest-framework serializer or lookup errors. For now permission denied and other exceptions will not use this error reporting, but a default one, for usage see tests.
```graphql
# default argument name is input
# default return field name is model name
mutation userCreate (input: {username: "myUsername"}) {
    user {
        id
        username
    }
    errors {
        field
        messages
    }
}


# Bulk operations return 'count' and errors
mutation userCreate (input: {username: "myUsername"}) {
    count
    errors {
        field
        messages
    }
}

# update mutations
# update and delete mutations by default specify lookup field 'id' or 'ids' for bulk mutations
mutation {
    userUpdate (id: 2, input: {username:"newUsername"} ) {
        user {
            id
            username
        }  
        errors {
            field
            messages
        }
    } 
}   


mutation {
    userBulkUpdate (ids: [2, 3], isActive: false ) {
        count
        errors {
           field
           messages
        }
    }
}  


# delete mutations
mutation {
    userDelete (id: 1) {
        user {
            id
        }
        errors {
           field
           messages
        }
    }
}  


mutation {
    userBulkDelete (ids: [1, 2, 3]) {
        count
        errors {
           field
           messages
        }
    }
}  
```

### Adding funcionality
All classes are derived from ```graphene.Mutation```. When you want to override some major functionality, the best place probabably is ```perform_mutate```, which is called after permission checks from graphene ```mutate```.  

In general probably the main functions that you want to override are: ```save()``` and ```get_object()``` for single object mutations or ```get_queryset()``` for bulk mutations.  
```get_object``` or ```get_queryset``` you should override to add more filters for fetching the object.   
```save``` performs final save/update/delete to database and you can add additional fields there.

Examples:
```python
# lets only update users that are inactive and add some random field
class UserUpdateInactiveMutation(mutations.UpdateModelMutation):
    class Meta:
        model = User

    @classmethod
    def get_object(cls, object_id, info, **input):
    # can get the object first and then check
        obj = super(UserUpdateInactiveMutation, cls).get_object(object_id, info, **input)
        if obj.is_active:
            return None
        return obj

    @classmethod
    def save(cls, serializer, root, info, **input):
        saved_object = serializer.save(updated_by=info.context.user)
        return cls.return_success(saved_object)


# same but for bulk mutation we have to override get_queryset
class UserBulkUpdateInactiveMutation(mutations.UpdateBulkModelMutation):
    class Meta:
        model = User

    @classmethod
    def get_queryset(cls, object_ids, info, **input):
        qs = super(UserBulkUpdateInactiveMutation, cls).get_queryset(object_ids, info, **input)
        qs.filter(is_active=False)
        return qs
```

For the whole function flow, please check the Base models in ```django_model_mutations\mutations.py```.
It was inspired by rest framework, so you can find functions like ```get_serializer_kwargs```, ```get_serializer```, ```validate_instance``` (for example here you can override default ```ValidationError``` exception and return None if you don't want exception of non existing id lookup etc.)

## Contributing
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

## License
[MIT](https://choosealicense.com/licenses/mit/)