active_graphql 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.hound.yml +4 -0
- data/.rspec +3 -0
- data/.rubocop.yml +48 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/CHANGELOG.md +25 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +134 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +6 -0
- data/active_graphql.gemspec +49 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/docs/.nojekyll +0 -0
- data/docs/README.md +95 -0
- data/docs/_sidebar.md +4 -0
- data/docs/client.md +69 -0
- data/docs/index.html +70 -0
- data/docs/model.md +464 -0
- data/lib/active_graphql.rb +10 -0
- data/lib/active_graphql/client.rb +38 -0
- data/lib/active_graphql/client/actions.rb +15 -0
- data/lib/active_graphql/client/actions/action.rb +116 -0
- data/lib/active_graphql/client/actions/action/format_inputs.rb +80 -0
- data/lib/active_graphql/client/actions/action/format_outputs.rb +40 -0
- data/lib/active_graphql/client/actions/mutation_action.rb +29 -0
- data/lib/active_graphql/client/actions/query_action.rb +23 -0
- data/lib/active_graphql/client/adapters.rb +10 -0
- data/lib/active_graphql/client/adapters/graphlient_adapter.rb +32 -0
- data/lib/active_graphql/client/response.rb +47 -0
- data/lib/active_graphql/errors.rb +11 -0
- data/lib/active_graphql/model.rb +174 -0
- data/lib/active_graphql/model/action_formatter.rb +96 -0
- data/lib/active_graphql/model/build_or_relation.rb +66 -0
- data/lib/active_graphql/model/configuration.rb +83 -0
- data/lib/active_graphql/model/find_in_batches.rb +54 -0
- data/lib/active_graphql/model/relation_proxy.rb +321 -0
- data/lib/active_graphql/version.rb +5 -0
- metadata +254 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
# GraphQL client which can be used to make requests to graphql endpoint
|
5
|
+
#
|
6
|
+
# Example usage:
|
7
|
+
# client = Client.new(url: 'http://example.com/graphql', headers: { 'Authorization' => 'secret'})
|
8
|
+
# client.query(:users).select(:name).result
|
9
|
+
class Client
|
10
|
+
require 'active_graphql/client/actions'
|
11
|
+
require 'active_graphql/client/adapters'
|
12
|
+
require 'active_graphql/client/response'
|
13
|
+
|
14
|
+
def initialize(config)
|
15
|
+
@config = config.dup
|
16
|
+
@adapter_class = @config.delete(:adapter)
|
17
|
+
end
|
18
|
+
|
19
|
+
def query(name)
|
20
|
+
Actions::QueryAction.new(name: name, client: adapter)
|
21
|
+
end
|
22
|
+
|
23
|
+
def mutation(name)
|
24
|
+
Actions::MutationAction.new(name: name, client: adapter)
|
25
|
+
end
|
26
|
+
|
27
|
+
def adapter
|
28
|
+
@adapter ||= begin
|
29
|
+
adapter_builder = @adapter_class || Adapters::GraphlientAdapter
|
30
|
+
adapter_builder.new(config)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_reader :config
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
# nodoc
|
6
|
+
module Actions
|
7
|
+
require 'active_graphql/errors'
|
8
|
+
require 'active_graphql/client/actions/action'
|
9
|
+
require 'active_graphql/client/actions/query_action'
|
10
|
+
require 'active_graphql/client/actions/mutation_action'
|
11
|
+
|
12
|
+
class WrongTypeError < ActiveGraphql::Errors::Error; end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Actions
|
6
|
+
# Base class for query/mutation action objects
|
7
|
+
class Action
|
8
|
+
class InvalidActionError < StandardError; end
|
9
|
+
|
10
|
+
require 'active_graphql/client/actions/action/format_outputs'
|
11
|
+
require 'active_graphql/client/actions/action/format_inputs'
|
12
|
+
|
13
|
+
attr_reader :name, :type, :output_values, :client, :input_attributes, :meta_attributes
|
14
|
+
|
15
|
+
delegate :result, :result!, to: :response
|
16
|
+
|
17
|
+
def initialize(name:, client:, output_values: [], input_attributes: {}, meta_attributes: {})
|
18
|
+
@name = name
|
19
|
+
@output_values = output_values
|
20
|
+
@input_attributes = input_attributes
|
21
|
+
@meta_attributes = meta_attributes
|
22
|
+
@client = client
|
23
|
+
end
|
24
|
+
|
25
|
+
def inspect
|
26
|
+
"#<#{self.class} " \
|
27
|
+
"name: #{name.inspect}, " \
|
28
|
+
"input: #{input_attributes.inspect}, " \
|
29
|
+
"output: #{output_values.inspect}, " \
|
30
|
+
"meta: #{meta_attributes.inspect}" \
|
31
|
+
'>'
|
32
|
+
end
|
33
|
+
|
34
|
+
def rewhere(**input_attributes)
|
35
|
+
chain(input_attributes: input_attributes)
|
36
|
+
end
|
37
|
+
|
38
|
+
def where(**extra_input_attributes)
|
39
|
+
rewhere(**input_attributes, **extra_input_attributes)
|
40
|
+
end
|
41
|
+
alias input where
|
42
|
+
|
43
|
+
def response
|
44
|
+
client.post(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
def meta(new_attributes)
|
48
|
+
chain(meta_attributes: meta_attributes.merge(new_attributes))
|
49
|
+
end
|
50
|
+
|
51
|
+
def reselect(*array_outputs, **hash_outputs)
|
52
|
+
outputs = join_array_and_hash(*array_outputs, **hash_outputs)
|
53
|
+
chain(output_values: outputs)
|
54
|
+
end
|
55
|
+
|
56
|
+
def select(*array_outputs, **hash_outputs)
|
57
|
+
full_array_outputs = (output_values + array_outputs).uniq
|
58
|
+
reselect(*full_array_outputs, **hash_outputs)
|
59
|
+
end
|
60
|
+
alias output select
|
61
|
+
|
62
|
+
def to_graphql
|
63
|
+
assert_format
|
64
|
+
|
65
|
+
<<~TXT
|
66
|
+
#{type} {
|
67
|
+
#{name}#{wrapped_header formatted_inputs} {
|
68
|
+
#{formatted_outputs}
|
69
|
+
}
|
70
|
+
}
|
71
|
+
TXT
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def join_array_and_hash(*array, **hash)
|
77
|
+
array + hash.map { |k, v| { k => v } }
|
78
|
+
end
|
79
|
+
|
80
|
+
def formatted_inputs
|
81
|
+
FormatInputs.new(input_attributes).call
|
82
|
+
end
|
83
|
+
|
84
|
+
def formatted_outputs
|
85
|
+
FormatOutputs.new(output_values).call
|
86
|
+
end
|
87
|
+
|
88
|
+
def assert_format
|
89
|
+
return unless output_values.empty?
|
90
|
+
|
91
|
+
raise(
|
92
|
+
InvalidActionError,
|
93
|
+
'at least one return value must be set. Do `query.select(:fields_to_return)` to do so'
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def chain(**new_values)
|
98
|
+
self.class.new(
|
99
|
+
name: name,
|
100
|
+
output_values: output_values,
|
101
|
+
input_attributes: input_attributes,
|
102
|
+
meta_attributes: meta_attributes,
|
103
|
+
client: client,
|
104
|
+
**new_values
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def wrapped_header(header_text)
|
109
|
+
return '' if header_text.empty?
|
110
|
+
|
111
|
+
"(#{header_text})"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Actions
|
6
|
+
class Action
|
7
|
+
# converts ruby object in to grapqhl input string
|
8
|
+
class FormatInputs
|
9
|
+
def initialize(inputs)
|
10
|
+
@inputs = inputs
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
return '' if inputs.empty?
|
15
|
+
|
16
|
+
formatted(inputs)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :inputs
|
22
|
+
|
23
|
+
def formatted(attributes)
|
24
|
+
if attributes.is_a?(Hash)
|
25
|
+
formatted_attributes(attributes)
|
26
|
+
else
|
27
|
+
raise(
|
28
|
+
ActiveGraphql::Client::Actions::WrongTypeError,
|
29
|
+
"Unsupported attribute type: #{attributes.inspect}:#{attributes.class}"
|
30
|
+
)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def formatted_attributes(attributes)
|
35
|
+
attributes = attributes.dup
|
36
|
+
keyword_fields = (attributes.delete(:__keyword_attributes) || []).map(&:to_s)
|
37
|
+
|
38
|
+
formatted_attributes = attributes.map do |key, val|
|
39
|
+
if keyword_fields.include?(key.to_s)
|
40
|
+
formatted_key_and_keyword(key, val)
|
41
|
+
else
|
42
|
+
formatted_key_and_value(key, val)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
formatted_attributes.join(', ')
|
47
|
+
end
|
48
|
+
|
49
|
+
def formatted_key_and_value(key, value)
|
50
|
+
"#{key}: #{formatted_value(value)}"
|
51
|
+
end
|
52
|
+
|
53
|
+
def formatted_key_and_keyword(key, value)
|
54
|
+
if value.is_a?(String) || value.is_a?(Symbol)
|
55
|
+
"#{key}: #{value}"
|
56
|
+
else
|
57
|
+
"#{key}: #{formatted_value(value)}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def formatted_value(value) # rubocop:disable Metrics/MethodLength
|
62
|
+
case value
|
63
|
+
when Hash
|
64
|
+
"{ #{formatted(value)} }"
|
65
|
+
when Array
|
66
|
+
formatted_values = value.map { |it| formatted_value(it) }
|
67
|
+
"[#{formatted_values.join(', ')}]"
|
68
|
+
when nil
|
69
|
+
'null'
|
70
|
+
when Symbol
|
71
|
+
value.to_s.inspect
|
72
|
+
else
|
73
|
+
value.inspect
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Actions
|
6
|
+
class Action
|
7
|
+
# converts ruby object in to grapqhl output string
|
8
|
+
class FormatOutputs
|
9
|
+
def initialize(outputs)
|
10
|
+
@outputs = outputs
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
Array.wrap(outputs).map { |it| formatted(it) }.join(', ')
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
attr_reader :outputs
|
20
|
+
|
21
|
+
def formatted(attribute) # rubocop:disable Metrics/MethodLength
|
22
|
+
case attribute
|
23
|
+
when Hash
|
24
|
+
attribute.map { |key, value| "#{key} { #{formatted(value)} }" }.join(', ')
|
25
|
+
when Symbol, String
|
26
|
+
attribute.to_s
|
27
|
+
when Array
|
28
|
+
attribute.map { |it| formatted(it) }.join(', ')
|
29
|
+
else
|
30
|
+
raise(
|
31
|
+
ActiveGraphql::Client::Actions::WrongTypeError,
|
32
|
+
"Unsupported attribute type: #{attribute.inspect}:#{attribute.class}"
|
33
|
+
)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Actions
|
6
|
+
# handles all action details which are specific for mutation type request
|
7
|
+
class MutationAction < Action
|
8
|
+
require 'active_graphql/errors'
|
9
|
+
|
10
|
+
class UnsuccessfullRequestError < ActiveGraphql::Errors::Error; end
|
11
|
+
|
12
|
+
def type
|
13
|
+
:mutation
|
14
|
+
end
|
15
|
+
|
16
|
+
def update(inputs)
|
17
|
+
where(inputs).response
|
18
|
+
end
|
19
|
+
|
20
|
+
def update!(inputs)
|
21
|
+
response = where(inputs).response
|
22
|
+
return response.result if response.success?
|
23
|
+
|
24
|
+
raise UnsuccessfullRequestError, response.errors.first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Actions
|
6
|
+
# handles all action details which are specific for query type request
|
7
|
+
class QueryAction < Action
|
8
|
+
def type
|
9
|
+
:query
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_by(inputs)
|
13
|
+
where(inputs).result
|
14
|
+
end
|
15
|
+
|
16
|
+
def select_paginated(*array_outputs, **hash_outputs)
|
17
|
+
outputs = join_array_and_hash(*array_outputs, **hash_outputs)
|
18
|
+
select(edges: { node: outputs })
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
module Adapters
|
6
|
+
# Client which makes raw API requests to GraphQL server
|
7
|
+
class GraphlientAdapter
|
8
|
+
require 'graphlient'
|
9
|
+
|
10
|
+
def initialize(config)
|
11
|
+
@url = config[:url]
|
12
|
+
@adapter_config = config.except(:url)
|
13
|
+
end
|
14
|
+
|
15
|
+
def post(action)
|
16
|
+
raw_response = graphql_client.query(action.to_graphql)
|
17
|
+
Response.new(raw_response.data)
|
18
|
+
rescue Graphlient::Errors::GraphQLError => e
|
19
|
+
Response.new(nil, e)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :url, :adapter_config
|
25
|
+
|
26
|
+
def graphql_client
|
27
|
+
@graphql_client ||= Graphlient::Client.new(url, **adapter_config)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGraphql
|
4
|
+
class Client
|
5
|
+
# graphql response wrapper
|
6
|
+
class Response
|
7
|
+
attr_reader :graphql_object
|
8
|
+
|
9
|
+
def initialize(graphql_object, error = nil)
|
10
|
+
if graphql_object
|
11
|
+
root_field = graphql_object.to_h.keys.first.to_s.underscore
|
12
|
+
@graphql_object = graphql_object.public_send(root_field)
|
13
|
+
end
|
14
|
+
|
15
|
+
@graphql_error = error if error
|
16
|
+
end
|
17
|
+
|
18
|
+
def result
|
19
|
+
graphql_object
|
20
|
+
end
|
21
|
+
|
22
|
+
def result!
|
23
|
+
raise ResponseError, errors.first if errors.any?
|
24
|
+
|
25
|
+
graphql_object
|
26
|
+
end
|
27
|
+
|
28
|
+
def success?
|
29
|
+
graphql_error.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
def detailed_errors
|
33
|
+
return [] if graphql_error.blank?
|
34
|
+
|
35
|
+
graphql_error.errors.details['data'].map(&:with_indifferent_access)
|
36
|
+
end
|
37
|
+
|
38
|
+
def errors
|
39
|
+
detailed_errors.map { |error| error[:message] }
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
attr_reader :graphql_error
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|