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!
}
Related
I'm not using auth, I've added a re_password field to my serializer, I think it only does a consistency check with the password field when a POST request comes in.
But the problem is that if re_password and password are write_only, then PUT and PATCH requests must also pass in these 2 fields.
I guess the consistency validation of re_password and password is reasonable for user registration, but it is not so necessary for updating user information.
What can I do to make re_password and password only required for POST requests?
POST: i need password and re_password field register new user account
PUT/PATCH: i don't need password and re_password as they are not suitable for updating user info
class UserSerializer(serializers.ModelSerializer):
re_password = serializers.CharField(write_only=True, min_length=6, max_length=20, error_messages={
"min_length": "Password at least 6 digits",
"max_length": "Password up to 20 characters",
})
class Meta:
exclude = ("is_delete",)
model = models.User
extra_kwargs = {**CommonSerializer.extra_kwargs, **{
"password": {
"write_only": True,
"min_length": 6,
"max_length": 20,
"error_messages": {
"min_length": "Password at least 6 digits",
"max_length": "Password up to 20 characters",
}
},
}}
def validate_password(self, data):
return hashlib.md5(data.encode("utf-8")).hexdigest()
def validate_re_password(self, data):
return hashlib.md5(data.encode("utf-8")).hexdigest()
def validate(self, validate_data):
if validate_data['password'] != validate_data.pop('re_password'):
raise exceptions.AuthenticationFailed("password not match")
return validate_data
def create(self, validate_data):
instance = models.User.objects.create(**validate_data)
return instance
def update(self, instance, validate_data):
password = validate_data.get("password")
validate_data["password"] = hashlib.md5(
password.encode("utf-8")).hexdigest()
for key, val in validate_data.items():
setattr(instance, key, val)
instance.save()
return instance
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
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.
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
}
}
}
While creating simple login api using DRF, I encountered a problem. Two field email and password are required to login. If the fields are left blank following json message is shown:
{
"email": [
"This field may not be blank."
],
"password": [
"This field may not be blank."
]
}
But I would like to customise the error message, say to something like,
{
"email": [
"Email field may not be blank."
],
"password": [
"Password field may not be blank."
]
}
I tried the something like the following in validate() in serializers.py :
if email is None:
raise serializers.ValidationError(
'An email address is required to log in.'
)
But it is not getting override, I'm not sure about the reason.
Edit
I implemented with #dima answer it still not work. What am I doing wrong?, now my serializer looks like:
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255, required=True, error_messages={"required": "Email field may not be blank."})
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True, required=True,
error_messages={"required": "Password field may not be blank."})
token = serializers.CharField(max_length=255, read_only=True)
def validate(self, data):
# The `validate` method is where we make sure that the current
# instance of `LoginSerializer` has "valid". In the case of logging a
# user in, this means validating that they've provided an email
# and password and that this combination matches one of the users in
# our database.
email = data.get('email', None)
password = data.get('password', None)
user = authenticate(username=email, password=password)
# If no user was found matching this email/password combination then
# `authenticate` will return `None`. Raise an exception in this case.
if user is None:
raise serializers.ValidationError(
'A user with this email and password was not found.'
)
# Django provides a flag on our `User` model called `is_active`. The
# purpose of this flag is to tell us whether the user has been banned
# or deactivated. This will almost never be the case, but
# it is worth checking. Raise an exception in this case.
if not user.is_active:
raise serializers.ValidationError(
'This user has been deactivated.'
)
# The `validate` method should return a dictionary of validated data.
# This is the data that is passed to the `create` and `update` methods
# that we will see later on.
return {
'email': user.email,
'username': user.username,
'token': user.token
}
views.py
class AuthLogin(APIView):
''' Manual implementation of login method '''
permission_classes = (AllowAny,)
serializer_class = LoginSerializer
def post(self, request, *args, **kwargs):
data = request.data
serializer = LoginSerializer(data=data)
if serializer.is_valid(raise_exception=True):
new_data = serializer.data
return Response(new_data)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
You can set error_messages attribute for fields you want to override message. In your case:
class LoginSerializer(serializers.Serializer):
email = serializers.CharField(max_length=255, required=True, error_messages={"required": "Email field may not be blank."})
username = serializers.CharField(max_length=255, read_only=True)
password = serializers.CharField(max_length=128, write_only=True, required=True, error_messages={"required": "Password field may not be blank."})
token = serializers.CharField(max_length=255, read_only=True)
For ModelSerializers you can do this using extra_kwargs property in Meta class.
class SomeModelSerializer(serializers.ModelSerializer):
class Meta:
model = SomeModel
fields = ('email', 'password')
extra_kwargs = {
'password': {"error_messages": {"required": "Password field may not be blank."}},
'email': {"error_messages": {"required": "Email field may not be blank."}},
}
you need field-level-validation, try it:
def validate_email(self, value):
# ^^^^^^
if not value:
raise serializers.ValidationError(
'An email address is required to log in.'
)
return value