Skip to content

Commit da575ba

Browse files
committed
Change README for SchemaPlugin instructions
1 parent 7cb6072 commit da575ba

6 files changed

Lines changed: 230 additions & 18 deletions

File tree

README.md

Lines changed: 160 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ GraphQL interface on top of the [Devise Token Auth](https://github.com/lynndylan
1212
* [Table of Contents](#table-of-contents)
1313
* [Introduction](#introduction)
1414
* [Installation](#installation)
15+
* [Important](#important)
1516
* [Usage](#usage)
16-
* [Mounting Routes manually](#mounting-routes-manually)
17-
* [Available Operations](#available-operations)
17+
* [Mounting Auth Schema on a Separate Route](#mounting-auth-schema-on-a-separate-route)
18+
* [Mounting Operations Into Your Own Schema](#mounting-operations-into-your-own-schema)
19+
* [Available Mount Options](#available-mount-options)
20+
* [Available Operations](#available-operations)
1821
* [Configuring Model](#configuring-model)
1922
* [Customizing Email Templates](#customizing-email-templates)
2023
* [I18n](#i18n)
2124
* [Authenticating Controller Actions](#authenticating-controller-actions)
25+
* [Authenticate Before Reaching Your GQL Schema](#authenticate-before-reaching-your-gql-schema)
26+
* [Authenticate in Your GQL Schema](#authenticate-in-your-gql-schema)
2227
* [Making Requests](#making-requests)
2328
* [Mutations](#mutations)
2429
* [Queries](#queries)
@@ -30,7 +35,7 @@ GraphQL interface on top of the [Devise Token Auth](https://github.com/lynndylan
3035
* [Contributing](#contributing)
3136
* [License](#license)
3237

33-
<!-- Added by: mcelicalderon, at: Tue Apr 28 21:43:30 -05 2020 -->
38+
<!-- Added by: mcelicalderon, at: Wed Jun 10 00:48:00 -05 2020 -->
3439

3540
<!--te-->
3641

@@ -87,14 +92,24 @@ Will do the following:
8792
`Admin` could be any model name you are going to be using for authentication,
8893
and `api/auth` could be any mount path you would like to use for auth.
8994

90-
**Important:** Remember this gem mounts a completely separate GraphQL schema on a separate controller in the route
91-
provided by the `at` option in the `mount_graphql_devise_for` method in the `config/routes.rb` file. If no `at`
92-
option is provided, the route will be `/graphql_auth`. This has no effect on your own application schema.
93-
More on this in the next section.
95+
### Important
96+
Remember this gem mounts a completely separate GraphQL schema on a separate controller in the route
97+
provided by the `at` option in the `mount_graphql_devise_for` method in the `config/routes.rb` file by default. If no `at`
98+
option is provided, the route will be `/graphql_auth`.
99+
100+
**Starting with `v0.12.0`** you can opt-in to a new behavior where you actually load this gem's
101+
queries and mutations into your own application's schema. If you do this, but also run the
102+
generator, you will have to remove the generated lines from your `config/routes.rb` file.
103+
You can actually mount a resource's auth schema in a separate route and in your app's
104+
schema at the same time, but that's probably not a common scenario. More on this in the next section.
94105

95106
## Usage
96-
### Mounting Routes manually
97-
Routes can be added using the initializer or manually.
107+
### Mounting Auth Schema on a Separate Route
108+
The generator can do this step for you by default. Remember now you can mount this gem's
109+
auth operations into your own schema as described in [this section](#mounting-operations-into-your-own-schema).
110+
111+
112+
Routes can be added using the generator or manually.
98113
You can mount this gem's GraphQL auth schema in your routes file like this:
99114

100115
```ruby
@@ -120,11 +135,84 @@ Rails.application.routes.draw do
120135
)
121136
end
122137
```
138+
The second argument of the `mount_graphql_devise` method is a hash of options where you can
139+
customize how the queries and mutations are mounted into the schema. For a list of available
140+
options go [here](#available-mount-options)
141+
142+
### Mounting Operations Into Your Own Schema
143+
Starting with `v0.12.0` you can now mount the GQL operations provided by this gem into your
144+
app's main schema. If you used the generator, remember that the mount method might have
145+
been included in your `config/routes.rb` file and you can remove it if you are using this
146+
mechanism.
147+
148+
```ruby
149+
# app/graphql/dummy_schema.rb
150+
151+
class DummySchema < GraphQL::Schema
152+
# It's important that this line goes before setting the query and mutation type on your
153+
# schema in graphql versions < 1.10.0
154+
use GraphqlDevise::SchemaPlugin.new(
155+
query: Types::QueryType,
156+
mutation: Types::MutationType,
157+
resource_loaders: [
158+
GraphqlDevise::ResourceLoader.new('User', only: [:login, :confirm_account])
159+
]
160+
)
161+
162+
mutation(Types::MutationType)
163+
query(Types::QueryType)
164+
end
165+
```
166+
The example above describes just one of the possible scenarios you might need.
167+
The second argument of the `GraphqlDevise::ResourceLoader` initializer is a hash of
168+
options where you can customize how the queries and mutations are mounted into the schema.
169+
For a list of available options go [here](#available-mount-options).
170+
171+
It's important to use the plugin in your schema before assigning the mutation and query type to
172+
it in graphql versions `< 1.10.0`. Otherwise the auth operations won't be available.
173+
174+
You can provide as many resource loaders as you need to the `resource_loaders` option, and each
175+
of those will be loaded into your schema. These are the options you can initialize the
176+
`SchemaPlugin` with:
177+
178+
1. `query`: This param is mandatory unless you skip all queries via the resource loader
179+
options. This should be the same `QueryType` you provide to the `query` method
180+
in your schema.
181+
1. `mutation`: This param mandatory unless you skip all mutations via the resource loader
182+
options. This should be the same `MutationType` you provide to the `mutation` method
183+
in your schema.
184+
1. `resource_loaders`: This is an optional array of `GraphqlDevise::ResourceLoader` instances.
185+
Here is where you specify the operations that you want to load into your app's schema.
186+
If no loader is provided no operations will be added to your schema, but you will still be
187+
able to authenticate queries and mutations selectively. More on this in the controller
188+
authentication [section](#authenticating-controller-actions).
189+
1. `authenticate_default`: This is a boolean value which is `true` by default. This value
190+
defines what is the default behavior for authentication in your schema fields. `true` means
191+
every root level field requires authentication unless specified otherwise using the
192+
`authenticate: false` option on the field. `false` means your root level fields won't require
193+
authentication unless specified otherwise using the `authenticate: true` option on the field.
194+
1. `unauthenticated_proc`: This param is optional. Here you can provide a proc that receives
195+
one argument (field name) and is called whenever a field that requires authentication
196+
is called without an authenticated resource.
197+
198+
### Available Mount Options
199+
Both the `mount_graphql_devise_for` method and the `GraphqlDevise::ResourceLoader` class
200+
take the same options. So, wether you decide to mount this gem in a separate route
201+
from your main application's schema or you use our `GraphqlDevise::SchemaPlugin` to load
202+
this gem's auth operation into your schema, these are the options you can provide as a hash.
123203

124-
Here are the options for the mount method:
204+
```ruby
205+
# Using the mount method in your config/routes.rb file
206+
mount_graphql_devise_for('User', {})
125207

126-
1. `at`: Route where the GraphQL schema will be mounted on the Rails server. In this example your API will have these two routes: `POST /api/v1/graphql_auth` and `GET /api/v1/graphql_auth`.
127-
If this option is not specified, the schema will be mounted at `/graphql_auth`.
208+
# Providing options to a GraphqlDevise::ResourceLoader
209+
GraphqlDevise::ResourceLoader.new('User', {})
210+
```
211+
212+
1. `at`: Route where the GraphQL schema will be mounted on the Rails server.
213+
In [this example](#mounting-auth-schema-on-a-separate-route) your API will have
214+
these two routes: `POST /api/v1/graphql_auth` and `GET /api/v1/graphql_auth`.
215+
If this option is not specified, the schema will be mounted at `/graphql_auth`. **This option only works if you are using the mount method.**
128216
1. `operations`: Specifying this is optional. Here you can override default
129217
behavior by specifying your own mutations and queries for every GraphQL operation.
130218
Check available operations in this file [mutations](https://github.com/graphql-devise/graphql_devise/blob/b5985036e01ea064e43e457b4f0c8516f172471c/lib/graphql_devise/rails/routes.rb#L19)
@@ -163,7 +251,7 @@ or [base resolver](https://github.com/graphql-devise/graphql_devise/blob/master/
163251
respectively, to take advantage of some of the methods provided by devise
164252
just like with `devise_scope`
165253

166-
#### Available Operations
254+
### Available Operations
167255
The following is a list of the symbols you can provide to the `operations`, `skip` and `only` options of the mount method:
168256
```ruby
169257
:login
@@ -175,7 +263,6 @@ The following is a list of the symbols you can provide to the `operations`, `ski
175263
:check_password_token
176264
```
177265

178-
179266
### Configuring Model
180267
Just like with Devise and DTA, you need to include a module in your authenticatable model,
181268
so with our example, your user model will have to look like this:
@@ -216,6 +303,9 @@ Keep in mind that if your app uses multiple locales, you should set the `I18n.lo
216303

217304
### Authenticating Controller Actions
218305
Just like with Devise or DTA, you will need to authenticate users in your controllers.
306+
For this you have two alternatives.
307+
308+
#### Authenticate Before Reaching Your GQL Schema
219309
For this you need to call `authenticate_<model>!` in a before_action hook of your controller.
220310
In our example our model is `User`, so it would look like this:
221311
```ruby
@@ -234,6 +324,62 @@ end
234324

235325
The install generator can do this for you because it executes DTA installer.
236326
See [Installation](#Installation) for details.
327+
If authentication fails for the request for whatever reason, execution of the request is halted
328+
and an error is returned in a REST format as the request never reaches your GQL schema.
329+
330+
#### Authenticate in Your GQL Schema
331+
For this you will need to add the `GraphqlDevise::SchemaPlugin` to your schema as described
332+
[here](#mounting-operations-into-your-own-schema) and also set the authenticated resource
333+
in a `before_action` hook.
334+
335+
```ruby
336+
# app/controllers/my_controller.rb
337+
338+
class MyController < ApplicationController
339+
include GraphqlDevise::Concerns::SetResourceByToken
340+
341+
before_action -> { set_resource_by_token(:user) }
342+
343+
def my_action
344+
render json: DummySchema.execute(params[:query], context: graphql_context)
345+
end
346+
end
347+
348+
# @resource.to_s.underscore.tr('/', '_').to_sym
349+
```
350+
The `set_resource_by_token` method receives a symbol identifying the resource you are trying
351+
to authenticate. So if you mounted the `'User'` resource, the symbol is `:user`. You can use
352+
this snippet to find the symbol for more complex scenarios
353+
`resource_klass.to_s.underscore.tr('/', '_').to_sym`.
354+
355+
The `graphql_context` method is simply a helper method that returns a hash like this
356+
```ruby
357+
{ current_resource: @resource, controller: self }
358+
```
359+
These are the two values the gem needs to check if a user is authenticated and to perform
360+
other auth operations. All `set_resource_by_token` does is set the `@resource` variable if
361+
the provided authentication headers are valid. If authentication fails, resource will be `nil`
362+
and this is how `GraphqlDevise::SchemaPlugin` knows if a user is authenticated or not in
363+
each query.
364+
365+
Please note that by using this mechanism your GQL schema will be in control of what queries are
366+
restricted to authenticated users and you can only do this at the root level fields of your GQL
367+
schema. Configure the plugin as explained [here](#mounting-operations-into-your-own-schema)
368+
so this can work.
369+
370+
In you main app's schema this is how you might specify if a field needs to be authenticated or not:
371+
```ruby
372+
module Types
373+
class QueryType < Types::BaseObject
374+
# user field used the default set in the Plugin's initializer
375+
field :user, resolver: Resolvers::UserShow
376+
# this field will never require authentication
377+
field :public_field, String, null: false, authenticate: false
378+
# this field requires authentication
379+
field :private_field, String, null: false, authenticate: true
380+
end
381+
end
382+
```
237383

238384
### Making Requests
239385
Here is a list of the available mutations and queries assuming your mounted model is `User`.
@@ -318,7 +464,6 @@ standard Devise templates.
318464
## Future Work
319465
We will continue to improve the gem and add better docs.
320466

321-
1. Add mount option that will create a separate schema for the mounted resource.
322467
1. Make sure this gem can correctly work alongside DTA and the original Devise gem.
323468
1. Improve DOCS.
324469
1. Add support for unlockable and other Devise modules.

lib/graphql_devise/resource_loader.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module GraphqlDevise
22
class ResourceLoader
3-
def initialize(resource, options, routing = false)
3+
def initialize(resource, options = {}, routing = false)
44
@resource = resource
55
@options = options
66
@routing = routing
@@ -33,6 +33,10 @@ def call(query, mutation)
3333

3434
prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type)
3535

36+
if prepared_resolvers.any? && query.blank?
37+
raise GraphqlDevise::Error, 'You need to provide a query type unless all queries are skipped'
38+
end
39+
3640
prepared_resolvers.each do |action, resolver|
3741
query.field(action, resolver: resolver, authenticate: false)
3842
end

lib/graphql_devise/schema_plugin.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module GraphqlDevise
22
class SchemaPlugin
33
DEFAULT_NOT_AUTHENTICATED = ->(field) { raise GraphqlDevise::UserError, "#{field} field requires authentication" }
44

5-
def initialize(query:, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
5+
def initialize(query: nil, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
66
@query = query
77
@mutation = mutation
88
@resource_loaders = resource_loaders

spec/dummy/app/graphql/types/mutation_type.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module Types
22
class MutationType < Types::BaseObject
3-
field :dummy_mutation, String, null: false
3+
field :dummy_mutation, String, null: false, authenticate: true
44

55
def dummy_mutation
66
'Necessary so GraphQL gem does not complain about empty mutation type'

spec/requests/user_controller_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,56 @@
8181
end
8282
end
8383

84+
describe 'dummyMutation' do
85+
let(:query) do
86+
<<-GRAPHQL
87+
mutation {
88+
dummyMutation
89+
}
90+
GRAPHQL
91+
end
92+
93+
context 'when using a regular schema' do
94+
before { post_request('/api/v1/graphql') }
95+
96+
context 'when user is authenticated' do
97+
let(:headers) { user.create_new_auth_token }
98+
99+
it 'allow to perform the query' do
100+
expect(json_response[:data][:dummyMutation]).to eq('Necessary so GraphQL gem does not complain about empty mutation type')
101+
end
102+
end
103+
104+
context 'when user is not authenticated' do
105+
it 'returns a must sign in error' do
106+
expect(json_response[:errors]).to contain_exactly(
107+
hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'USER_ERROR' })
108+
)
109+
end
110+
end
111+
end
112+
113+
context 'when using an interpreter schema' do
114+
before { post_request('/api/v1/interpreter') }
115+
116+
context 'when user is authenticated' do
117+
let(:headers) { user.create_new_auth_token }
118+
119+
it 'allow to perform the query' do
120+
expect(json_response[:data][:dummyMutation]).to eq('Necessary so GraphQL gem does not complain about empty mutation type')
121+
end
122+
end
123+
124+
context 'when user is not authenticated' do
125+
it 'returns a must sign in error' do
126+
expect(json_response[:errors]).to contain_exactly(
127+
hash_including(message: 'dummyMutation field requires authentication', extensions: { code: 'USER_ERROR' })
128+
)
129+
end
130+
end
131+
end
132+
end
133+
84134
describe 'user' do
85135
let(:query) do
86136
<<-GRAPHQL

spec/services/resource_loader_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@
3939
end
4040
end
4141

42+
context 'when query is nil' do
43+
let(:query) { nil }
44+
45+
before { allow(mutation).to receive(:field) }
46+
47+
it 'raises an error' do
48+
expect { loader }.to raise_error(
49+
GraphqlDevise::Error,
50+
'You need to provide a query type unless all queries are skipped'
51+
)
52+
end
53+
end
54+
4255
context 'when invoked from router' do
4356
let(:routing) { true }
4457

0 commit comments

Comments
 (0)