Skip to content

Commit 6e7af85

Browse files
Merge pull request #64 from graphql-devise/additional-mutations-queries
Add additional mutations and queries option
2 parents 135227c + 13804bd commit 6e7af85

8 files changed

Lines changed: 184 additions & 8 deletions

File tree

README.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Will do the following:
4646
- Add `devise` modules to `Admin` model
4747
- Other changes that you can find [here](https://devise-token-auth.gitbook.io/devise-token-auth/config)
4848
- Add the route to `config/routes.rb`
49-
- `mount_graphql_devise_for 'Admin', at: 'api/auth'
49+
- `mount_graphql_devise_for 'Admin', at: 'api/auth'`
5050

5151
`Admin` could be any model name you are going to be using for authentication,
5252
and `api/auth` could be any mount path you would like to use for auth.
@@ -66,7 +66,15 @@ Rails.application.routes.draw do
6666
operations: {
6767
login: Mutations::Login
6868
},
69-
skip: [:sign_up]
69+
skip: [:sign_up],
70+
additional_mutations: {
71+
# generates mutation { adminUserSignUp }
72+
admin_user_sign_up: Mutations::AdminUserSignUp
73+
},
74+
additional_queries: {
75+
# generates query { publicUserByUuid }
76+
public_user_by_uuid: Resolvers::UserByUuid
77+
}
7078
)
7179
end
7280
```
@@ -90,6 +98,28 @@ will use it. But, you can override this type with this option like in the exampl
9098
symbols and should belong to the list of available operations in the gem.
9199
1. `only`: An array of the operations that should be available in the authentication schema. The `skip` and `only` options are
92100
mutually exclusive, an error will be raised if you pass both to the mount method.
101+
1. `additional_mutations`: Here you can add as many mutations as you
102+
need, for those features that don't fully match the provided default mutations and queries.
103+
You need to provide a hash to this option, and
104+
each key will be the name of the mutation on the schema. Also, the value provided must be a valid mutation.
105+
This is similar to what you can accomplish with
106+
[devise_scope](https://www.rubydoc.info/github/heartcombo/devise/master/ActionDispatch/Routing/Mapper%3Adevise_for).
107+
1. `additional_queries`: Here you can add as many queries as you need,
108+
for those features that don't fully match the provided default mutations and queries.
109+
You need to provide a hash to this option, and
110+
each key will be the name of the query on the schema. Also, the value provided must be a valid Resolver.
111+
This is also similar to what you can accomplish with
112+
[devise_scope](https://www.rubydoc.info/github/heartcombo/devise/master/ActionDispatch/Routing/Mapper%3Adevise_for).
113+
114+
Additional mutations and queries will be added to the schema regardless
115+
of other options you might have specified like `skip` or `only`.
116+
Additional queries and mutations is usually a good place for other
117+
operations on your schema that require no authentication (like sign_up).
118+
Also by adding them through the mount method, your mutations and
119+
resolvers can inherit from our [base mutation](https://github.com/graphql-devise/graphql_devise/blob/master/app/graphql/graphql_devise/mutations/base.rb)
120+
or [base resover](https://github.com/graphql-devise/graphql_devise/blob/master/app/graphql/graphql_devise/resolvers/base.rb)
121+
respectively, to take advantage of some of the methods provided by devise
122+
just like with `devise_scope`
93123

94124
#### Available Operations
95125
The following is a list of the symbols you can provide to the `operations`, `skip` and `only` options of the mount method:

app/graphql/graphql_devise/mutations/sign_up.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class SignUp < Base
77
argument :confirm_success_url, String, required: false
88

99
def resolve(confirm_success_url: nil, **attrs)
10-
resource = resource_class.new(provider: provider, **attrs)
10+
resource = build_resource(attrs.merge(provider: provider))
1111
raise_user_error(I18n.t('graphql_devise.resource_build_failed')) if resource.blank?
1212

1313
redirect_url = confirm_success_url || DeviseTokenAuth.default_confirm_success_url
@@ -45,6 +45,10 @@ def resolve(confirm_success_url: nil, **attrs)
4545

4646
private
4747

48+
def build_resource(attrs)
49+
resource_class.new(attrs)
50+
end
51+
4852
def provider
4953
:email
5054
end

lib/graphql_devise/rails/routes.rb

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
module ActionDispatch::Routing
22
class Mapper
33
def mount_graphql_devise_for(resource, opts = {})
4-
custom_operations = opts[:operations] || {}
5-
skipped_operations = opts.fetch(:skip, [])
6-
only_operations = opts.fetch(:only, [])
4+
custom_operations = opts.fetch(:operations, {})
5+
skipped_operations = opts.fetch(:skip, [])
6+
only_operations = opts.fetch(:only, [])
7+
additional_mutations = opts.fetch(:additional_mutations, {})
8+
additional_queries = opts.fetch(:additional_queries, {})
79

810
if [skipped_operations, only_operations].all?(&:any?)
911
raise GraphqlDevise::Error, "Can't specify both `skip` and `only` options when mounting the route."
@@ -62,8 +64,11 @@ def mount_graphql_devise_for(resource, opts = {})
6264

6365
GraphqlDevise::Types::MutationType.field("#{mapping_name}_#{action}", mutation: used_mutation)
6466
end
67+
additional_mutations.each do |action, mutation|
68+
GraphqlDevise::Types::MutationType.field(action, mutation: mutation)
69+
end
6570

66-
if used_mutations.present? &&
71+
if (used_mutations.present? || additional_mutations.present?) &&
6772
(Gem::Version.new(GraphQL::VERSION) <= Gem::Version.new('1.10.0') || GraphqlDevise::Schema.mutation.nil?)
6873
GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
6974
end
@@ -87,8 +92,11 @@ def mount_graphql_devise_for(resource, opts = {})
8792

8893
GraphqlDevise::Types::QueryType.field("#{mapping_name}_#{action}", resolver: used_query)
8994
end
95+
additional_queries.each do |action, resolver|
96+
GraphqlDevise::Types::QueryType.field(action, resolver: resolver)
97+
end
9098

91-
if used_queries.blank? && GraphqlDevise::Types::QueryType.fields.blank?
99+
if (used_queries.blank? || additional_queries.present?) && GraphqlDevise::Types::QueryType.fields.blank?
92100
GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy)
93101
end
94102

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
module Mutations
2+
class RegisterConfirmedUser < GraphqlDevise::Mutations::Base
3+
argument :email, String, required: true
4+
argument :name, String, required: true
5+
argument :password, String, required: true
6+
argument :password_confirmation, String, required: true
7+
8+
field :user, Types::UserType, null: true
9+
10+
def resolve(**attrs)
11+
user = User.new(attrs.merge(confirmed_at: Time.zone.now))
12+
13+
if user.save
14+
{ user: user }
15+
else
16+
raise_user_error_list(
17+
'Custom registration failed',
18+
errors: user.errors.full_messages
19+
)
20+
end
21+
end
22+
end
23+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module Resolvers
2+
class PublicUser < GraphQL::Schema::Resolver
3+
type Types::UserType, null: false
4+
5+
argument :id, Int, required: true
6+
7+
def resolve(id:)
8+
User.find(id)
9+
end
10+
end
11+
end

spec/dummy/config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
mount_graphql_devise_for 'User', at: '/api/v1/graphql_auth', operations: {
33
login: Mutations::Login,
44
sign_up: Mutations::SignUp
5+
}, additional_mutations: {
6+
register_confirmed_user: Mutations::RegisterConfirmedUser
7+
}, additional_queries: {
8+
public_user: Resolvers::PublicUser
59
}
610

711
mount_graphql_devise_for(
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe 'Additional Mutations' do
4+
include_context 'with graphql query request'
5+
6+
let(:name) { Faker::Name.name }
7+
let(:password) { Faker::Internet.password }
8+
let(:password_confirmation) { password }
9+
let(:email) { Faker::Internet.email }
10+
let(:redirect) { Faker::Internet.url }
11+
12+
context 'when using the user model' do
13+
let(:query) do
14+
<<-GRAPHQL
15+
mutation {
16+
registerConfirmedUser(
17+
email: "#{email}",
18+
name: "#{name}",
19+
password: "#{password}",
20+
passwordConfirmation: "#{password_confirmation}"
21+
) {
22+
user {
23+
email
24+
name
25+
}
26+
}
27+
}
28+
GRAPHQL
29+
end
30+
31+
context 'when params are correct' do
32+
it 'creates a new resource that is already confirmed' do
33+
expect { post_request }.to(
34+
change(User, :count).by(1)
35+
.and(not_change(ActionMailer::Base.deliveries, :count))
36+
)
37+
38+
user = User.last
39+
40+
expect(user).to be_confirmed
41+
expect(json_response[:data][:registerConfirmedUser]).to include(
42+
user: {
43+
email: email,
44+
name: name
45+
}
46+
)
47+
end
48+
end
49+
50+
context 'when params are incorrect' do
51+
let(:password_confirmation) { 'not the same' }
52+
53+
it 'returns descriptive errors' do
54+
expect { post_request }.to not_change(User, :count)
55+
56+
expect(json_response[:errors]).to contain_exactly(
57+
hash_including(
58+
message: 'Custom registration failed',
59+
extensions: { code: 'USER_ERROR', detailed_errors: ["Password confirmation doesn't match Password"] }
60+
)
61+
)
62+
end
63+
end
64+
end
65+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
require 'rails_helper'
2+
3+
RSpec.describe 'Additional Queries' do
4+
include_context 'with graphql query request'
5+
6+
let(:public_user) { create(:user, :confirmed) }
7+
8+
context 'when using the user model' do
9+
let(:query) do
10+
<<-GRAPHQL
11+
query {
12+
publicUser(
13+
id: #{public_user.id}
14+
) {
15+
email
16+
name
17+
}
18+
}
19+
GRAPHQL
20+
end
21+
22+
before { post_request }
23+
24+
it 'fetches a user by ID' do
25+
expect(json_response[:data][:publicUser]).to include(
26+
email: public_user.email,
27+
name: public_user.name
28+
)
29+
end
30+
end
31+
end

0 commit comments

Comments
 (0)