Skip to content

Commit

Permalink
Add SchemaPlugin
Browse files Browse the repository at this point in the history
  • Loading branch information
mcelicalderon committed Jun 9, 2020
1 parent 2e63cda commit 6898d21
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 25 deletions.
4 changes: 2 additions & 2 deletions lib/graphql_devise/resource_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ def call(query, mutation)
end

prepared_mutations.each do |action, prepared_mutation|
mutation.field(action, mutation: prepared_mutation)
mutation.field(action, mutation: prepared_mutation, authenticate: false)
end

prepared_resolvers = prepare_resolvers(mapping_name, clean_options, authenticatable_type)

prepared_resolvers.each do |action, resolver|
query.field(action, resolver: resolver)
query.field(action, resolver: resolver, authenticate: false)
end

GraphqlDevise.add_mapping(mapping_name, @resource)
Expand Down
74 changes: 70 additions & 4 deletions lib/graphql_devise/schema_plugin.rb
Original file line number Diff line number Diff line change
@@ -1,16 +1,79 @@
module GraphqlDevise
class SchemaPlugin
def initialize(query:, mutation: nil, resource_loaders: [])
@query = query
@mutation = mutation
@resource_loaders = resource_loaders
DEFAULT_NOT_AUTHENTICATED = ->(field) { raise GraphqlDevise::UserError, "#{field} field requires authentication" }

def initialize(query:, mutation: nil, authenticate_default: true, resource_loaders: [], unauthenticated_proc: DEFAULT_NOT_AUTHENTICATED)
@query = query
@mutation = mutation
@resource_loaders = resource_loaders
@authenticate_default = authenticate_default
@unauthenticated_proc = unauthenticated_proc

# Must happen on initialize so operations are loaded before the types are added to the schema on GQL < 1.10
load_fields
end

def use(schema_definition)
schema_definition.tracer(self)
end

def trace(event, trace_data)
# Authenticate only root level queries
return yield unless event == 'execute_field' && path(trace_data).count == 1

field = traced_field(trace_data)
provided_value = authenticate_option(field, trace_data)

if (!provided_value.nil? && provided_value) || @authenticate_default
raise_on_missing_resource(
context(trace_data),
field
)
end

yield
end

private

def raise_on_missing_resource(context, field)
@unauthenticated_proc.call(field.name) if context[:current_resource].blank?
end

def context(trace_data)
query = if trace_data[:context]
trace_data[:context].query
else
trace_data[:query]
end

query.context
end

def path(trace_data)
if trace_data[:context]
trace_data[:context].path
else
trace_data[:path]
end
end

def traced_field(trace_data)
if trace_data[:context]
trace_data[:context].field
else
trace_data[:field]
end
end

def authenticate_option(field, trace_data)
if trace_data[:context]
field.metadata[:authenticate]
else
field.graphql_definition.metadata[:authenticate]
end
end

def load_fields
@resource_loaders.each do |resource_loader|
raise Error, 'Invalid resource loader instance' unless resource_loader.instance_of?(GraphqlDevise::ResourceLoader)
Expand All @@ -20,3 +83,6 @@ def load_fields
end
end
end

GraphQL::Field.accepts_definitions(authenticate: GraphQL::Define.assign_metadata_key(:authenticate))
GraphQL::Schema::Field.accepts_definition(:authenticate)
8 changes: 6 additions & 2 deletions spec/dummy/app/controllers/api/v1/graphql_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@ module V1
class GraphqlController < ApplicationController
include GraphqlDevise::Concerns::SetUserByToken

before_action :authenticate_user!
before_action -> { set_user_by_token(:user) }

def graphql
render json: DummySchema.execute(params[:query])
render json: DummySchema.execute(params[:query], context: { current_resource: @resource, controller: self })
end

def interpreter
render json: InterpreterSchema.execute(params[:query], context: { current_resource: @resource, controller: self })
end

private
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/app/graphql/dummy_schema.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class DummySchema < GraphQL::Schema
use GraphqlDevise::SchemaPlugin.new(query: Types::QueryType)

mutation(Types::MutationType)
query(Types::QueryType)
end
9 changes: 9 additions & 0 deletions spec/dummy/app/graphql/interpreter_schema.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class InterpreterSchema < GraphQL::Schema
use GraphQL::Execution::Interpreter if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.9.0')
use GraphQL::Analysis::AST if Gem::Version.new(GraphQL::VERSION) >= Gem::Version.new('1.10.0')

use GraphqlDevise::SchemaPlugin.new(query: Types::QueryType)

mutation(Types::MutationType)
query(Types::QueryType)
end
1 change: 1 addition & 0 deletions spec/dummy/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@
)

post '/api/v1/graphql', to: 'api/v1/graphql#graphql'
post '/api/v1/interpreter', to: 'api/v1/graphql#interpreter'
end
51 changes: 38 additions & 13 deletions spec/requests/user_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,49 @@
GRAPHQL
end

before { post_request('/api/v1/graphql') }
context 'when using a regular schema' do
before { post_request('/api/v1/graphql') }

context 'when user is authenticated' do
let(:headers) { user.create_new_auth_token }
context 'when user is authenticated' do
let(:headers) { user.create_new_auth_token }

it 'allow to perform the query' do
expect(json_response[:data][:user]).to match(
email: user.email,
id: user.id
)
it 'allow to perform the query' do
expect(json_response[:data][:user]).to match(
email: user.email,
id: user.id
)
end
end

context 'when user is not authenticated' do
it 'returns a must sign in error' do
expect(json_response[:errors]).to contain_exactly(
hash_including(message: 'user field requires authentication', extensions: { code: 'USER_ERROR' })
)
end
end
end

context 'when user is not authenticated' do
it 'returns a must sign in error' do
expect(json_response[:errors]).to contain_exactly(
'You need to sign in or sign up before continuing.'
)
context 'when using an interpreter schema' do
before { post_request('/api/v1/interpreter') }

context 'when user is authenticated' do
let(:headers) { user.create_new_auth_token }

it 'allow to perform the query' do
expect(json_response[:data][:user]).to match(
email: user.email,
id: user.id
)
end
end

context 'when user is not authenticated' do
it 'returns a must sign in error' do
expect(json_response[:errors]).to contain_exactly(
hash_including(message: 'user field requires authentication', extensions: { code: 'USER_ERROR' })
)
end
end
end
end
8 changes: 4 additions & 4 deletions spec/services/resource_loader_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
end

it 'loads operations into the provided types' do
expect(query).to receive(:field).with(:user_confirm_account, resolver: instance_of(Class))
expect(mutation).to receive(:field).with(:user_login, mutation: instance_of(Class))
expect(query).to receive(:field).with(:user_confirm_account, resolver: instance_of(Class), authenticate: false)
expect(mutation).to receive(:field).with(:user_login, mutation: instance_of(Class), authenticate: false)
expect(GraphqlDevise).to receive(:add_mapping).with(:user, resource)
expect(GraphqlDevise).not_to receive(:mount_resource)

Expand Down Expand Up @@ -58,8 +58,8 @@
before { allow(GraphqlDevise).to receive(:resource_mounted?).with(:user).and_return(true) }

it 'skips schema loading' do
expect(query).not_to receive(:field).with(:user_confirm_account, resolver: instance_of(Class))
expect(mutation).not_to receive(:field).with(:user_login, mutation: instance_of(Class))
expect(query).not_to receive(:field)
expect(mutation).not_to receive(:field)
expect(GraphqlDevise).not_to receive(:add_mapping).with(:user, resource)
expect(GraphqlDevise).not_to receive(:mount_resource)
end
Expand Down

0 comments on commit 6898d21

Please sign in to comment.