graphql code organization for scalable projects - python

Think that we have a big project with lots of apps which results in lots of queries and mutations. For such projects, how do you people handle the graphql code architecture. Let's take an example.
I have an app called accounts. It will have queries and mutation related to user and profile. The folder structure i am using is every app will have graphql folder which then will have schema.py and mutations.py. The code is arranged something like this for now
schema.py
class User(DjangoObjectType):
class Meta:
model = CustomUser
filter_fields = {
'email': ['exact', ],
}
exclude_fields = ('password', 'is_superuser', )
interfaces = (relay.Node, )
class ProfileNode(DjangoObjectType):
class Meta:
model = Profile
interfaces = (relay.Node, )
class UserQuery(object):
user = relay.Node.Field(User)
users = DjangoFilterConnectionField(User) # resolve_users is not needed now
class ProfileQuery(object):
profile = relay.Node.Field(ProfileNode)
profiles = DjangoFilterConnectionField(ProfileNode)
class UserProfile(ObjectType):
profile = Field(ProfileNode)
def resolve_profile(self, info, **kwargs):
if id is not None and info.context.user.is_authenticated:
profile = Profile.objects.get(user=info.context.user)
return profile
return None
class Viewer(ObjectType):
user = Field(User)
def resolve_user(self, info, **kwargs):
if info.context.user.is_authenticated:
return info.context.user
return None
mutations.py
class Register(graphene.Mutation):
"""
Mutation to register a user
"""
class Arguments:
first_name = graphene.String(required=True)
last_name = graphene.String(required=True)
email = graphene.String(required=True)
password = graphene.String(required=True)
password_repeat = graphene.String(required=True)
success = graphene.Boolean()
errors = graphene.List(graphene.String)
def mutate(self, info, first_name, last_name, email, password, password_repeat):
# console.log('info', info, first_name, last_name, email, password)
if password == password_repeat:
try:
user = CustomUser.objects.create(
first_name=first_name,
last_name=last_name,
email=email,
is_active=False
)
print ('user', user)
user.set_password(password)
user.save()
if djoser_settings.get('SEND_ACTIVATION_EMAIL'):
send_activation_email(user, info.context)
return Register(success=bool(user.id))
# TODO: specify exception
except Exception:
errors = ["email", "Email already registered."]
return Register(success=False, errors=errors)
errors = ["password", "Passwords don't match."]
return Register(success=False, errors=errors)
root schema
// just to show the number of mutations just for account apps.
from accounts.graphql.mutations import (
Activate,
DeleteAccount,
Login,
RefreshToken,
Register,
ResetPassword,
ResetPasswordConfirm,
)
from accounts.graphql.schema import Viewer, UserProfile
class Mutation(company_mutation.Mutation, graphene.ObjectType):
activate = Activate.Field()
debug = graphene.Field(DjangoDebug, name='__debug')
class Query(company_schema.Query, graphene.ObjectType):
viewer = graphene.Field(Viewer)
user_profile = graphene.Field(UserProfile)
debug = graphene.Field(DjangoDebug, name='__debug')
#staticmethod
def resolve_viewer(self, info, **kwargs):
if info.context.user.is_authenticated:
return info.context.user
return None
#staticmethod
def resolve_user_profile(self, info, **kwargs):
if info.context.user.is_authenticated and id:
return info.context.user
return None
schema = graphene.Schema(query=Query, mutation=Mutation)
yo can see the mutations just for accounts app. There are many mutations and there will be more when considering all the apps. How you people are dealing with such?

I think project organization based only on files like schema.py, queries.py, mutations.py is very bad for large project.
It's like organize your model parts, with files like models.py, fields.py, utils.py ...
Especially, a GraphQL ObjectType defined somewhere in your queries.py file can be returned or used as input by a mutation.
So I prefer a structure more based on objects and their logical relations
schema/
__init__.py : contains your root schema actual code
viewer.py : contains the ObjectType Viewer
user/
user.py : contains the ObjectType User and UserQuery
profile.py : contains the ObjectType Profil
account : contains the account management mutations
login.py : contains the login / logout mutations
...
Note : you can embed mutations the same way you did queries, to be able to get mutations queries like :
mutation {
account {
delete(id: 12) {
status
}
}
}

Related

How to show specific field only to the user profile owner in graphene-django?

I have the following schema in my graphene-django application:
import graphene
from django.contrib.auth import get_user_model
from graphene_django import DjangoObjectType
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
fields = ("id", "username", "email")
class Query(object):
user = graphene.Field(UserType, user_id=graphene.Int())
def resolve_user(self, info, user_id):
user = get_user_model().objects.get(pk=user_id)
if info.context.user.id != user_id:
# If the query didn't access email field -> query is ok
# If the query tried to access email field -> raise an error
else:
# Logged in as the user we're querying -> let the query access all the fields
I want to be able to query the schema in the following way:
# Logged in as user 1 => no errors, because we're allowed to see all fields
query {
user (userId: 1) {
id
username
email
}
}
# Not logged in as user 1 => no errors, because not trying to see email
query {
user (userId: 1) {
id
username
}
}
# Not logged in as user 1 => return error because accessing email
query {
user (userId: 1) {
id
username
email
}
}
How can I make it so that only a logged in user can see the email field of their own profile and no one else can see the emails of others?
Here's the approach I would take based on the comments. The main issue here is to be able to get a list of fields requested by a query in the resolver. For that, I use a code adapted from here:
def get_requested_fields(info):
"""Get list of fields requested in a query."""
fragments = info.fragments
def iterate_field_names(prefix, field):
name = field.name.value
if isinstance(field, FragmentSpread):
results = []
new_prefix = prefix
sub_selection = fragments[name].selection_set.selections
else:
results = [prefix + name]
new_prefix = prefix + name + '.'
sub_selection = \
field.selection_set.selections if field.selection_set else []
for sub_field in sub_selection:
results += iterate_field_names(new_prefix, sub_field)
return results
results = iterate_field_names('', info.field_asts[0])
return results
The rest should be quite straightforward:
import graphene
from django.contrib.auth import get_user_model
from graphene_django import DjangoObjectType
class AuthorizationError(Exception):
"""Authorization failed."""
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
fields = ("id", "username", "email")
class Query(object):
user = graphene.Field(UserType, user_id=graphene.Int())
def resolve_user(self, info, user_id):
user = get_user_model().objects.get(pk=user_id)
if info.context.user.id != user_id:
fields = get_requested_fields(info)
if 'user.email' in fields:
raise AuthorizationError('Not authorized to access user email')
return user
I ended up just doing it like this, where the actual value of email is returned when querying one's own info, and None is returned for others:
import graphene
from django.contrib.auth import get_user_model
from graphene_django import DjangoObjectType
class UserType(DjangoObjectType):
class Meta:
model = get_user_model()
fields = ("id", "username", "email")
def resolve_email(self, info):
if info.context.user.is_authenticated and self.pk == info.context.user.pk:
return self.email
else:
return None
class Query(graphene.ObjectType):
user = graphene.Field(UserType, user_id=graphene.Int())
def resolve_user(self, info, user_id):
return get_user_model().objects.get(pk=user_id)
The current answer is wayyy overcomplicated. Just create two ObjectTypes e.g.:
class PublicUserType(DjangoObjectType):
class Meta:
model = get_user_model()
fields = ('id', 'username')
class PrivateUserType(DjangoObjectType):
class Meta:
model = get_user_model()
Spent over 4 hours trying other solutions before realized it was this simple

"Unknown type \"CreateAccountInput\". with graphene_django when trying to mutate/create new object

I'm new to graphQL and I've just been trying to create a new account object with it. This is my code right now but it the GUI for graphQL I keep getting the "Unknown type \"CreateAccountInput" error.
schema.py
from authentication.models import Account
import graphene
from graphene import relay, ObjectType
from graphene_django import DjangoObjectType
from graphene import InputObjectType
class AccountGraphQL(DjangoObjectType):
class Meta:
model = Account
interfaces = (relay.Node, )
class Query(graphene.ObjectType):
username = graphene.String(argument=graphene.String(default_value="stranger"))
email = graphene.String(argument=graphene.String(default_value="stranger"))
password = graphene.String(argument=graphene.String(default_value="stranger"))
first_name = graphene.String(argument=graphene.String(default_value="stranger"))
info = graphene.String()
def def_resolve_create_account(self, info, argument):
return account.objects.create_user()
class CreateAccountInput(InputObjectType):
username = graphene.String(argument=graphene.String(default_value="stranger"))
email = graphene.String(argument=graphene.String(default_value="stranger"))
password = graphene.String(argument=graphene.String(default_value="stranger"))
first_name = graphene.String(argument=graphene.String(default_value="stranger"))
class CreateAccount(relay.ClientIDMutation):
class Input:
account = graphene.Argument(CreateAccountInput)
new_account = graphene.Field(AccountGraphQL)
#classmethod
def mutate_and_get_payload(cls, args, context, info):
account_data = args.get('account') # get account data from args
account = Account()
new_account = update_create_instance(account, account_data)
return cls(new_account=new_account)
class Mutation(ObjectType):
create_account = CreateAccount.Field()
schema = graphene.Schema(query=Query)
result = schema.execute('{ create_account (argument: "graphql"')
my mutation:
mutation CreateNewAccount($input: CreateAccountInput!) {
createAccount(input: $input) {
newAccount {
username
email
password
first_name
}
clientMutationId
}
}
my query
{"input": {"account":
{
"username": "graphql",
"email": "graphql#gmail.com",
"password": "graphql",
"first_name":"charles"
}
}}
I'm not sure what the problem is here, as far as I'm concerned I've defined the input type
For user registration with graphQL you can use django-graphql-auth.
Here is the demo from the docs:
It covers user registration and account verification
(open the video on a new tab)
I wrote this package, any questions, talk to me.

Django Rest Framework use source to access different model with reverse relation?

I have the following problem, I have the default User model and Profile model. I want to merge them into one serializer but without nesting - It's just ugly. Instead, I want to have all their fields on the first level. So I created the following (for simplicity profile contains just one Bool field and one relation field):
class UserSerializer(serializers.ModelSerializer):
achievements = serializers.PrimaryKeyRelated(many=True, queryset=Achievements.objects.all())
trusted = serializers.BooleanField()
def create(self, validated_data):
user=User.objects.create_user(
password = validated_data['password'],
username = validated_data['username'],
email = validated_data['email'],
)
Profile.objects.update_or_create(user, defaults={
'trusted': validated_data['trusted'],
'achievements': validatd_data['achievements'],
}
)
return user
class Meta:
model = User
fields = ("id", "username", "email", "password", "trusted", "achievements"),
read_only = ("id",)
extra_kwargs = {
'password': {
'write_only': True,
},
}
Profile is connected to a user via a user field containing models.OneToOneField instance.
When I try to list all profiles I get Error that I need to specify source but I have no idea how and documentation mentions only something that dot notation should be used.
Thanks.
source is argument of serializer field. You should do something like this:
class UserSerializer(serializers.ModelSerializer):
achievements = serializers.PrimaryKeyRelated(many=True, queryset=Achievements.objects.all(), source='profile.achievements')
trusted = serializers.BooleanField(source='profile.trusted')
def create(self, validated_data):
user=User.objects.create_user(
password = validated_data['password'],
username = validated_data['username'],
email = validated_data['email'],
)
Profile.objects.update_or_create(user, defaults={
'trusted': validated_data['trusted'],
'achievements': validatd_data['achievements'],
}
)
return user

How do you validate a GraphQL mutation in Python

I am new to GraphQL. I am using Graphene-Django and have a mutation called CreateUser. It takes three arguments username, email, password.
How do I validate the data and return multiple errors back?
I want something like this returned.
{
"name":[
"Ensure this field has at least 2 characters."
],
"email":[
"This field may not be blank."
],
"password":[
"This field may not be blank."
]
}
So I can render the errors on the form like this:
My code so far:
from django.contrib.auth.models import User as UserModel
from graphene_django import DjangoObjectType
import graphene
class User(DjangoObjectType):
class Meta:
model = UserModel
only_fields = 'id', 'username', 'email'
class Query(graphene.ObjectType):
users = graphene.List(User)
user = graphene.Field(User, id=graphene.Int())
def resolve_users(self, info):
return UserModel.objects.all()
def resolve_user(self, info, **kwargs):
try:
return UserModel.objects.get(id=kwargs['id'])
except (UserModel.DoesNotExist, KeyError):
return None
class CreateUser(graphene.Mutation):
class Arguments:
username = graphene.String()
email = graphene.String()
password = graphene.String()
user = graphene.Field(User)
def mutate(self, info, username, email, password):
user = UserModel.objects.create_user(username=username, email=email, password=password)
return CreateUser(user=user)
class Mutation(graphene.ObjectType):
create_user = CreateUser.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
Model errors in the schema, primarily by using unions.
In SDL format:
type RegisterUserSuccess {
user: User!
}
type FieldError {
fieldName: String!
errors: [String!]!
}
type RegisterUserError {
fieldErrors: [FieldError!]!
nonFieldErrors: [String!]!
}
union RegisterUserPayload = RegisterUserSuccess | RegisterUserError
mutation {
registerUser(name: String, email: String, password: String): RegisterUserPayload!
}

Django - Creating two user types, where one type can be both

We're required to have two separate forms for two different types of users. Call them Client and Provider. Client would be the parent, base user, while Provider is a sort of extension. At any point a Client could become a Provider as well, while still maintaining status and information as a Client. So a Provider has both permissions as a Client and as a Provider.
I'm new to Django. All we're trying to do is register either user type, but have a one to one relation between Provider and Client tables if a user registers as a Provider straight away.
The issue we're having is in the adapter, we think. A provider registers fine, but ends up in the users_user table with no entry in the generated users_provider table. Is it the way we're trying to save and relate these two entities in the database, or something else?
We're trying to utilize allauth for authentication and registration.
Our code:
models.py:
class User(AbstractUser):
name = models.CharField(_('Name of User'), blank=True, max_length=255)
def __str__(self):
return self.username
def get_absolute_url(self):
return reverse('users:detail', kwargs={'username': self.username})
SEX = (
("M","MALE"),
("F","FEMALE"),
)
birthdate = models.DateField(_('Birth Date'), default=django.utils.timezone.now, blank=False)
sex = models.CharField(_('Sex'), choices=SEX, max_length=1, default="M")
isProvider = models.BooleanField(_('Provider'), default=False)
#Using User, not models.Model
class Provider(User):
HAS_BUSINESS = (
('YES','YES'),
('NO','NO'),
)
#Resolving asociation 1:1 to User
#NOTE: AUTH_USER_MODEL = users.User in setting
owner = models.OneToOneField(settings.AUTH_USER_MODEL)
has_business = models.CharField(_('Do you have your own business?'),max_length=2, choices=HAS_BUSINESS, default='NO')
isProvider = True
our forms.py
class ProviderForm(SignupForm,ModelForm):
name = forms.CharField(label='Name', strip=True, max_length=50)
lastname = forms.CharField(label='Last Name', strip=True, max_length=50)
Provider.isProvider = True
class Meta:
model = Provider
fields = '__all__'
exclude = GENERAL_EXCLUSIONS + [
'owner',
]
class ClientForm(SignupForm,ModelForm):
name = forms.CharField(label='Name', strip=True, max_length=50)
lastname = forms.CharField(label='Last Name', strip=True, max_length=50)
class Meta:
model = User
fields = "__all__"
exclude = GENERAL_EXCLUSIONS
def is_active(self):
return False
def __init__(self, *args, **kwargs):
super(ClientForm, self).__init__(*args, **kwargs)
views.py:
class ProviderRegisterView(SignupView):
template_name = 'account/form_provider.html'
form_class = ProviderForm
redirect_field_name = 'next'
view_name = 'registerprovider'
success_url = None
def get_context_data(self, **kwargs):
ret = super(ProviderRegisterView, self).get_context_data(**kwargs)
ret.update(self.kwargs)
return ret
registerprovider = ProviderRegisterView.as_view()
#View para el formulario de registro de usuarios clientes
class ClientRegisterView(SignupView):
template_name = 'account/form_client.html'
form_class = ClientForm
redirect_field_name = 'next'
view_name = 'registerclient'
success_url = None
def get_context_data(self, **kwargs):
ret = super(ClienteRegisterView, self).get_context_data(**kwargs)
ret.update(self.kwargs)
return ret
registerclient = ClienteRegisterView.as_view()
finally, our adapter.py:
#Per allauth documentation, settings changed:
#ACCOUNT_ADAPTER = 'projectname.users.adapters.RegisterUserAdapter'
class RegisterUserAdapter(DefaultAccountAdapter):
def save_user(self, request, user, form, commit=True):
data = form.cleaned_data
user.first_name = data['name']
user.last_name = data['lastname']
#Saving Client info
user.sex = data['sex']
user.birthdate = data['birthdate']
#Normal allauth saves
user.username = data['username']
user.email = data['email']
if user.isProvider:
p = Provider()
p.owner = user
p.has_business = data['has_business']
if 'password1' in data:
user.set_password(data['password1'])
else:
user.set_unusable_password()
self.populate_username(request, user)
if commit:
#Save user
user.save()
#If it's also a Provider, save the Provider
if user.isProvider:
p.save()
return user
Any help or tips would be greatly appreciated. If I left something out, please let me know. I'm not sure if the problem is in the model itself, the way we represent the form, or the adapter. The way it stands, it doesn't matter what form we use, it's always saved as the base User table (our Client) and the Provider table never gets information saved to it.
With Django's new custom user model, only one user model can be set as settings.AUTH_USER_MODEL. In your example, you can set this to your User model.
Then for the optional provider data, create a separate model that is referenced by OneToOneField from your User model.
class User(AbstractUser):
...
provider = models.OneToOneField(Provider, null=True)
class Provider(models.Model):
...
This is the easiest way to work with multiple user types in Django, given the AUTH_USER_MODEL constraint.
Also, it's best to only subclass abstract models, otherwise you get multitable inheritance which results in hidden implied JOINs, degrading performance.
Finally, you can create the Provider object in your custom form's form.is_valid() method and assign user.provider = provider.

Categories

Resources