NOTE: This is still proposal and subject to change
We are working on new API and first prototype is already available on next branch. New API is more type oriented and it's quite similar with strawberry-graphql pydantic API. The plan is to provide all the functionalities which old API provides today.
Changelog
- 17.3.2021 - first draft
- 19.3.2021 - new content
- 20.3.2021 - implementation status update
- 23.3.2021 - query, mutation and type register updates
- 24.3.2021 - type definition updates, relational_field removed, create_batch renamed to create
Type definitions
Library provides type and input type generation from Django models.
Example Django models
class User(models.Model):
name = models.CharField(max_length=50)
group = models.ForeignKey('Group', null=True, related_name='users', on_delete=models.CASCADE)
tag = models.OneToOneField('Tag', null=True, on_delete=models.CASCADE)
class Group(models.Model):
name = models.CharField(max_length=50)
tags = models.ManyToManyField('Tag', related_name='groups')
class Tag(models.Model):
name = models.CharField(max_length=50)
Defining types (implemented)
# types are generated with selected fields from models
@strawberry_django.type(models.User, fields=['id', 'name'])
class User:
# types can be extended
@strawberry.field
def name_upper(root) -> str:
return root.name.upper()
# strawberry_django provides default resolvers for relation fields for your convenience
group: 'Group' = strawberry_django.field()
# forward referencing is supported and field name is configurable :)
user_group: 'Group' = strawberry_django.field(field_name='group')
# all fields can be remapped
my_name: str = strawberry_django.field(field_name='name')
# field can be used as a decorator. Resolver function is guarded with Django's asgiref.sync.sync_to_async
# helper in async context, which means that it is safe to access Django ORM from resolver function
@strawberry_django.field
def user_group_resolver(root, info) -> 'Group':
return model.Group.objects.get(user__id=root.id)
# async resolvers are supported too but then it's user's responsibility to use sync_to_async wrapper
# with Django ORM
@strawberry_django.field
async def user_group_async_resolver(root, info) -> 'Group':
from asgiref.sync import sync_to_async
return sync_to_async(model.Group.objects.get)(user__id=root.id)
@strawberry_django.type(models.Group)
class Group:
pass
Defining input types (implemented)
@strawberry_django.input(models.User, fields=['group', 'tag'])
class UserInput:
name: str = strawberry_django.field()
@strawberry_django.input(models.Group)
class GroupInput:
pass
@strawberry_django.input(models.Tag, is_update=True)
class TagUpdateInput:
pass
Validators and pre/post processors (not implemented)
We are discussing about adding this into strawberry core. See strawberry-graphql/strawberry#788
def field_validator(value, info):
if not info.context.request.user.has_permission():
raise Exception('Permission denied')
return value
@strawberry_django.input(models.User)
class UserInput:
field: str = strawberry_django.field(validators=[field_validator])
name: str = strawberry_django.field()
@name.validator
def name_validator(value):
if 'bad' in value:
raise ValueError('name contains word "bad"')
return value.upper()
# we can use validators also from django, wow!
url: str = strawberry_django.field(validators=[url.validator(django.core.validators.URLValidator()])
Defining queries (implemented)
# option 1
@strawberry.type
class Query:
user = strawberry_django.queries.get(User)
users = strawberry_django.queries.list(User)
# option 2 (queries parameter not implemented yet)
Query = strawberry_django.queries(User, queries=['get', 'list'])
Defining mutations (implemented)
# option 1
@strawberry.type
class Mutation:
create_users = strawberry_django.mutations.create(User, UserInput)
update_users = strawberry_django.mutations.update(User, UserInput)
delete_users = strawberry_django.mutations.delete(User, UserInput)
# option 2 (mutations parameter not implemented yet)
Mutation = strawberry_django.mutations(User, UserInput, mutations=['create', 'update', 'delete'])
schema = strawberry.Schema(query=Query, mutation=Mutation)
Query and mutation hooks (implemented)
Query hooks
- pre/post query (not implemented yet)
- queryset
- etc
Mutation hooks:
- pre/post save
- pre/update update (not implemented yet)
- pre/post delete (not implemented yet)
- etc
@strawberry.type
class Query:
groups = strawberry_django.queries.list(Group)
@groups.queryset
def groups_queryset(info, qs):
user = info.context.request.user
if not user.is_admin():
qs = qs.filter(user__id=user.id)
return qs
def group_post_save(info, instances):
logger.info('saved')
@strawberry.type
class Mutation:
create_groups = strawberry_django.mutations.create(GroupInput, Group, post_save=group_post_save)
@create_group.pre_save
def group_pre_save(info, instances):
instance.name = instance.name.upper()
Type registers (implemented)
Type register can be used to extend internal django model field map.
types = strawberry_django.TypeRegister()
@types.register(django.db.models.JsonField):
class MyJsonType:
string: str
integer: int
Model types can be registered in type register. Type register can be passed to type converters and query or mutation generators. Converters and generators use register to resolve field types.
@types.register
@strawberry_django.type(models.User, types=types)
class User:
pass
@types.register
@strawberry_django.type(models.Groups, types=types)
class Group:
pass
Type register can be passed to queries and mutations as well
@strawberry.type
class Query:
user = strawberry_django.queries.get(models.User, types=types)
groups = strawberry_django.queries.list(models.Group, types=types)
@strawberry.type
class Mutation:
create_users = strawberry_django.mutations.create(models.User, types=types)
update_users = strawberry_django.mutations.update(models.User, types=types)
delete_users = strawberry_django.mutations.delete(models.User, types=types)
# or even simpler
Query = strawberry_django.queries(models.User, models.Group, types=types)
Mutation = strawberry_django.mutations(models.User, models.Group, types=types)
Queries
Library generates schema with relation field resolvers from given types
Basic queries (implemented)
query {
user(id: 1) {
...
}
tags {
...
}
}
Reverse relations (implemented)
query {
group(id: 3) {
users {
...
}
}
}
Filtering (partially implemented)
NOTE: Current implementation uses list of strings like this filters: [ "name__contains='user'", "id__in!=[1,2,3]"]
.
Plan is to start implement graphql types for filters instead of using list of strings.
query {
users(filters: { name__contains: "user" }) {
id
name
tags(filters: { or: { id__gt: 20, not: { id__in: [5, 7, 10] } } }) {
id
}
}
}
Ordering (implemented)
query {
users(orderBy: ['-name']) {
...
}
}
Pagination (not implemented)
TBD
Mutations
Creating objects (implemented)
mutation {
createUsers(data: [{ name: "my name", groupId: 5 }]) {
...
}
createUsers(data: [{ name: "user1" }, { name: "user2" }]) {
...
}
}
Updating objects (implemented)
mutation {
# update basic and foreign key fields
updateUsers(data: { name: "user", groupId: 5}, filters: { ... }) {
...
}
# adding, setting and deleting many to many relations
updateGroups(data: { tagsAdd: [1, 2], tagsSet: [3, 4], tagsRemove: [5] }, filters: { ... }) {
...
}
}
Deleting objects (implemented)
mutation {
# returns list of deleted ids
deleteUsers(filters: { id__in: [1, 5, 9] })
}
Prorotype and example project
More detailed example is available on the next branch:
Tests are also good place to start from:
Feel free to leave any comments or feedback.