graphql_connector 0.2.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +37 -0
- data/.rspec +1 -0
- data/.rubocop_todo.yml +3 -0
- data/CHANGELOG.md +38 -9
- data/Gemfile.lock +30 -30
- data/README.md +155 -13
- data/examples/departments_service_class_examples.rb +51 -0
- data/examples/mutation_examples.rb +17 -0
- data/examples/query_examples.rb +21 -0
- data/examples/raw_query_examples.rb +24 -0
- data/graphql_connector.gemspec +7 -7
- data/lib/graphql_connector.rb +10 -21
- data/lib/graphql_connector/base_server_type.rb +62 -0
- data/lib/graphql_connector/configuration.rb +14 -3
- data/lib/graphql_connector/formatters/base_format.rb +81 -0
- data/lib/graphql_connector/formatters/mutation_format.rb +14 -0
- data/lib/graphql_connector/formatters/query_format.rb +14 -0
- data/lib/graphql_connector/http_client.rb +49 -0
- data/lib/graphql_connector/service_classable/class_method_validator.rb +31 -0
- data/lib/graphql_connector/service_classable/params_validator.rb +24 -0
- data/lib/graphql_connector/service_classable/queryable.rb +108 -0
- data/lib/graphql_connector/service_classable/return_fields_validator.rb +43 -0
- data/lib/graphql_connector/version.rb +1 -1
- metadata +27 -27
- data/.travis.yml +0 -9
- data/lib/graphql_connector/query_builder.rb +0 -59
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
4
|
+
# ! The following examples are used together with
|
5
|
+
# ! https://github.com/sushie1984/rails-graphql-server
|
6
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
7
|
+
|
8
|
+
uri = 'http://rails-graphql-server.herokuapp.com/api/graphql'
|
9
|
+
GraphqlConnector.configure do |config|
|
10
|
+
config.add_server(name: 'RailsGraphqlServer', uri: uri, headers: {})
|
11
|
+
end
|
12
|
+
|
13
|
+
department_input = { attributes: { name: 'One', location: 'Berlin' } }
|
14
|
+
return_fields = ['department': ['id', 'name', 'employees' => ['yearlySalary']]]
|
15
|
+
GraphqlConnector::RailsGraphqlServer.mutation('createDepartment',
|
16
|
+
{ input: department_input },
|
17
|
+
return_fields)
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
4
|
+
# ! The following examples are used together with
|
5
|
+
# ! https://github.com/sushie1984/rails-graphql-server
|
6
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
7
|
+
|
8
|
+
uri = 'http://rails-graphql-server.herokuapp.com/api/graphql'
|
9
|
+
GraphqlConnector.configure do |config|
|
10
|
+
config.add_server(name: 'RailsGraphqlServer', uri: uri, headers: {})
|
11
|
+
end
|
12
|
+
|
13
|
+
GraphqlConnector::RailsGraphqlServer.query('departments',
|
14
|
+
{},
|
15
|
+
['id', 'name',
|
16
|
+
'employees' => ['yearlySalary']])
|
17
|
+
|
18
|
+
GraphqlConnector::RailsGraphqlServer.query('departments',
|
19
|
+
{ id: %w[1 2] },
|
20
|
+
['id', 'name',
|
21
|
+
'employees' => ['yearlySalary']])
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
4
|
+
# ! The following examples are used together with
|
5
|
+
# ! https://github.com/sushie1984/rails-graphql-server
|
6
|
+
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
7
|
+
|
8
|
+
GraphqlConnector.configure do |config|
|
9
|
+
config.add_server(name: 'RailsGraphqlServer',
|
10
|
+
uri: 'http://rails-graphql-server.herokuapp.com/api/graphql',
|
11
|
+
headers: {})
|
12
|
+
end
|
13
|
+
|
14
|
+
GraphqlConnector::RailsGraphqlServer.raw_query(
|
15
|
+
'query { departments { id name employees { yearlySalary } } }'
|
16
|
+
)
|
17
|
+
|
18
|
+
GraphqlConnector::RailsGraphqlServer.raw_query(
|
19
|
+
'query departments($id: [ID!]) {
|
20
|
+
departments(id: $id) { name employees { name }
|
21
|
+
}
|
22
|
+
}',
|
23
|
+
variables: { id: %w[1 2] }
|
24
|
+
)
|
data/graphql_connector.gemspec
CHANGED
@@ -7,12 +7,13 @@ require 'graphql_connector/version'
|
|
7
7
|
Gem::Specification.new do |spec|
|
8
8
|
spec.name = 'graphql_connector'
|
9
9
|
spec.version = GraphqlConnector::VERSION
|
10
|
-
spec.authors = [
|
11
|
-
spec.email = ['palluthe.bennet@gmail.com']
|
10
|
+
spec.authors = %w[Garllon sushie1984]
|
11
|
+
spec.email = ['palluthe.bennet@gmail.com', 'sascha_burku@yahoo.de']
|
12
12
|
|
13
|
-
spec.summary = '
|
14
|
-
spec.description = '
|
15
|
-
'
|
13
|
+
spec.summary = 'GraphQL client'
|
14
|
+
spec.description = 'Grahql client to query with your own raw string, '\
|
15
|
+
'with the small helper method query or with service '\
|
16
|
+
'class inclusion.'
|
16
17
|
spec.homepage = 'https://github.com/Garllon/graphql_connector/blob/master/README.md'
|
17
18
|
spec.license = 'MIT'
|
18
19
|
|
@@ -30,11 +31,10 @@ Gem::Specification.new do |spec|
|
|
30
31
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
31
32
|
spec.require_paths = ['lib']
|
32
33
|
|
33
|
-
spec.add_dependency 'httparty', '~> 0.
|
34
|
+
spec.add_dependency 'httparty', '~> 0.16'
|
34
35
|
|
35
36
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
36
37
|
spec.add_development_dependency 'pry', '~> 0.10'
|
37
|
-
spec.add_development_dependency 'rake', '~> 10.0'
|
38
38
|
spec.add_development_dependency 'rspec', '~> 3.8'
|
39
39
|
spec.add_development_dependency 'rubocop', '~> 0.75'
|
40
40
|
end
|
data/lib/graphql_connector.rb
CHANGED
@@ -1,8 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'graphql_connector/version'
|
4
|
-
require 'graphql_connector/
|
4
|
+
require 'graphql_connector/formatters/base_format'
|
5
|
+
require 'graphql_connector/formatters/query_format'
|
6
|
+
require 'graphql_connector/formatters/mutation_format'
|
5
7
|
require 'graphql_connector/configuration'
|
8
|
+
require 'graphql_connector/http_client'
|
9
|
+
require 'graphql_connector/base_server_type'
|
10
|
+
require 'graphql_connector/service_classable/class_method_validator'
|
11
|
+
require 'graphql_connector/service_classable/params_validator'
|
12
|
+
require 'graphql_connector/service_classable/return_fields_validator'
|
13
|
+
require 'graphql_connector/service_classable/queryable'
|
6
14
|
require 'graphql_connector/custom_attribute_error'
|
7
15
|
require 'httparty'
|
8
16
|
|
@@ -19,29 +27,10 @@ module GraphqlConnector
|
|
19
27
|
end
|
20
28
|
|
21
29
|
def self.reset
|
22
|
-
@configuration
|
30
|
+
@configuration.reset!
|
23
31
|
end
|
24
32
|
|
25
33
|
def self.configure
|
26
34
|
yield(configuration)
|
27
35
|
end
|
28
|
-
|
29
|
-
def self.query(model, conditions, selected_fields)
|
30
|
-
query_string = QueryBuilder.new(model, conditions, selected_fields).create
|
31
|
-
parsed_body = raw_query(query_string)
|
32
|
-
OpenStruct.new(parsed_body['data'][model])
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.raw_query(query_string)
|
36
|
-
response = HTTParty.post(GraphqlConnector.configuration.host,
|
37
|
-
headers: GraphqlConnector.configuration.headers,
|
38
|
-
body: { query: query_string })
|
39
|
-
parsed_body = JSON.parse(response.body)
|
40
|
-
|
41
|
-
if parsed_body.key? 'errors'
|
42
|
-
raise CustomAttributeError, parsed_body['errors']
|
43
|
-
end
|
44
|
-
|
45
|
-
parsed_body
|
46
|
-
end
|
47
36
|
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlConnector
|
4
|
+
class BaseServerTypeAlreadyExistsError < StandardError; end
|
5
|
+
# Class to wrap http_client calls under a specific namespaced class
|
6
|
+
class BaseServerType
|
7
|
+
class << self
|
8
|
+
def build(name, uri, headers)
|
9
|
+
verify_new_client_type_for!(name)
|
10
|
+
base_class = class_with(uri, headers)
|
11
|
+
base_object = GraphqlConnector.const_set(name, base_class)
|
12
|
+
inject_http_client_delegations(base_object)
|
13
|
+
create_service_class_module(base_object)
|
14
|
+
|
15
|
+
base_object
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def verify_new_client_type_for!(name)
|
21
|
+
return unless GraphqlConnector.const_defined?(name)
|
22
|
+
|
23
|
+
raise BaseServerTypeAlreadyExistsError,
|
24
|
+
"The name: #{name} is already in use. Check your "\
|
25
|
+
'configuration!'
|
26
|
+
end
|
27
|
+
|
28
|
+
def create_service_class_module(base_object)
|
29
|
+
base_object.class_eval <<-METHOD, __FILE__, __LINE__ + 1
|
30
|
+
module Query
|
31
|
+
def self.extended(base)
|
32
|
+
base.extend(GraphqlConnector::ServiceClassable::Queryable)
|
33
|
+
end
|
34
|
+
|
35
|
+
def http_client
|
36
|
+
#{base_object}.http_client
|
37
|
+
end
|
38
|
+
end
|
39
|
+
METHOD
|
40
|
+
end
|
41
|
+
|
42
|
+
def class_with(uri, headers)
|
43
|
+
Class.new do
|
44
|
+
attr_accessor :uri, :headers
|
45
|
+
@uri = uri
|
46
|
+
@headers = headers
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def inject_http_client_delegations(base_object)
|
51
|
+
base_object.instance_eval do
|
52
|
+
extend SingleForwardable
|
53
|
+
def_delegators :http_client, :query, :raw_query, :mutation
|
54
|
+
|
55
|
+
def http_client
|
56
|
+
@http_client ||= GraphqlConnector::HttpClient.new(@uri, @headers)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -3,11 +3,22 @@
|
|
3
3
|
module GraphqlConnector
|
4
4
|
# The configuration template file for the gem.
|
5
5
|
class Configuration
|
6
|
-
|
6
|
+
attr_reader :base_server_types
|
7
7
|
|
8
8
|
def initialize
|
9
|
-
@
|
10
|
-
|
9
|
+
@base_server_types = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def add_server(name:, uri:, headers:)
|
13
|
+
@base_server_types[name] = BaseServerType.build(name, uri, headers)
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset!
|
17
|
+
@base_server_types.keys.each do |name|
|
18
|
+
GraphqlConnector.const_get(name).send :remove_const, 'Query'
|
19
|
+
GraphqlConnector.send :remove_const, name
|
20
|
+
end
|
21
|
+
@base_server_types = {}
|
11
22
|
end
|
12
23
|
end
|
13
24
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlConnector
|
4
|
+
module Formatters
|
5
|
+
# Class that returns in query or mutation string format
|
6
|
+
class BaseFormat
|
7
|
+
def initialize(model, conditions, selected_fields)
|
8
|
+
@model = model
|
9
|
+
@conditions = conditions
|
10
|
+
@selected_fields = selected_fields
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
<<-STRING
|
15
|
+
#{query_type} {
|
16
|
+
#{@model}#{arguments} {
|
17
|
+
#{parse_fields(@selected_fields)}
|
18
|
+
}
|
19
|
+
}
|
20
|
+
STRING
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def arguments
|
26
|
+
conditions = @conditions.each_with_object([]) do |(key, value), array|
|
27
|
+
array << "#{key}: #{value_as_parameter(value)}"
|
28
|
+
end
|
29
|
+
|
30
|
+
return '' if conditions.empty?
|
31
|
+
|
32
|
+
"(#{conditions.join(', ')})"
|
33
|
+
end
|
34
|
+
|
35
|
+
def value_as_parameter(value)
|
36
|
+
case value
|
37
|
+
when Array
|
38
|
+
casted_values = value.map { |v| value_as_parameter(v) }
|
39
|
+
"[#{casted_values.join(',')}]"
|
40
|
+
when Hash
|
41
|
+
casted_values = value.map { |k, v| "#{k}: #{value_as_parameter(v)}" }
|
42
|
+
"{#{casted_values.join(',')}}"
|
43
|
+
else
|
44
|
+
scalar_types(value)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def scalar_types(value)
|
49
|
+
case value
|
50
|
+
when TrueClass, FalseClass, Integer, Float
|
51
|
+
value
|
52
|
+
else # fallback to string
|
53
|
+
'"' + value.to_s + '"'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_fields(selected_fields)
|
58
|
+
results = selected_fields.map do |field|
|
59
|
+
case field
|
60
|
+
when Hash
|
61
|
+
handle_association(field)
|
62
|
+
else
|
63
|
+
field
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
results.join(' ')
|
68
|
+
end
|
69
|
+
|
70
|
+
def handle_association(hash)
|
71
|
+
hash.map do |key, fields|
|
72
|
+
"#{key} { #{parse_fields(fields)} }"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def query_type
|
77
|
+
raise 'query_type undefined'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlConnector
|
4
|
+
# Wrapper class for HTTParty post query
|
5
|
+
class HttpClient
|
6
|
+
def initialize(uri, headers)
|
7
|
+
@uri = uri
|
8
|
+
@headers = headers
|
9
|
+
end
|
10
|
+
|
11
|
+
def query(model, conditions, selected_fields)
|
12
|
+
query_string =
|
13
|
+
Formatters::QueryFormat.new(model, conditions, selected_fields).create
|
14
|
+
parsed_body = raw_query(query_string)
|
15
|
+
format_body(parsed_body['data'][model.to_s])
|
16
|
+
end
|
17
|
+
|
18
|
+
def mutation(model, inputs, selected_fields)
|
19
|
+
query_string =
|
20
|
+
Formatters::MutationFormat.new(model, inputs, selected_fields).create
|
21
|
+
parsed_body = raw_query(query_string)
|
22
|
+
format_body(parsed_body['data'][model.to_s])
|
23
|
+
end
|
24
|
+
|
25
|
+
def raw_query(query_string, variables: {})
|
26
|
+
response = HTTParty.post(@uri,
|
27
|
+
headers: @headers,
|
28
|
+
body: { query: query_string,
|
29
|
+
variables: variables })
|
30
|
+
parsed_body = JSON.parse(response.body)
|
31
|
+
verify_response!(parsed_body)
|
32
|
+
parsed_body
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def format_body(response_body)
|
38
|
+
return OpenStruct.new(response_body) unless response_body.is_a? Array
|
39
|
+
|
40
|
+
response_body.map { |entry| OpenStruct.new(entry) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def verify_response!(parsed_body)
|
44
|
+
return unless parsed_body.key? 'errors'
|
45
|
+
|
46
|
+
raise CustomAttributeError, parsed_body['errors']
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlConnector
|
4
|
+
module ServiceClassable
|
5
|
+
class ClassMethodAlreadyImplementedError < StandardError; end
|
6
|
+
class InvalidClassTypeError < StandardError; end
|
7
|
+
# Checks whether a class method for a specific graphql query is in an
|
8
|
+
# expected format
|
9
|
+
class ClassMethodValidator
|
10
|
+
class << self
|
11
|
+
def validate_class_method(class_method_name, invoked_class)
|
12
|
+
return unless invoked_class.singleton_methods
|
13
|
+
.map(&:to_s)
|
14
|
+
.include?(class_method_name.to_s)
|
15
|
+
|
16
|
+
error_msg = "The method '#{class_method_name}: ... ' is "\
|
17
|
+
'already implemented within the context of '\
|
18
|
+
"#{invoked_class} and therefore cannot be used again!"
|
19
|
+
raise ClassMethodAlreadyImplementedError, error_msg
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate_element_class_type(element, class_types)
|
23
|
+
return if element.class == class_types
|
24
|
+
|
25
|
+
raise InvalidClassTypeError, "Please ensure that #{element} is a"\
|
26
|
+
"#{class_types}!"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GraphqlConnector
|
4
|
+
module ServiceClassable
|
5
|
+
class InvalidParamsError < StandardError; end
|
6
|
+
# Checks whether params for a specifc graphql query are in an expected
|
7
|
+
# format
|
8
|
+
class ParamsValidator
|
9
|
+
class << self
|
10
|
+
def validate(query_params)
|
11
|
+
params = [query_params].flatten
|
12
|
+
return if params.empty? ||
|
13
|
+
params.map(&:class).uniq == [Symbol] ||
|
14
|
+
params.map(&:class).uniq == [String]
|
15
|
+
|
16
|
+
raise InvalidParamsError,
|
17
|
+
"Please ensure that #{query_params} are either "\
|
18
|
+
'Symbols/Strings as described in the README '\
|
19
|
+
'(e.g.: params: [:id, :name])'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|