active_graphql 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.hound.yml +4 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +48 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/CHANGELOG.md +25 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/Gemfile +15 -0
  11. data/Gemfile.lock +134 -0
  12. data/LICENSE.txt +21 -0
  13. data/Rakefile +6 -0
  14. data/active_graphql.gemspec +49 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docs/.nojekyll +0 -0
  18. data/docs/README.md +95 -0
  19. data/docs/_sidebar.md +4 -0
  20. data/docs/client.md +69 -0
  21. data/docs/index.html +70 -0
  22. data/docs/model.md +464 -0
  23. data/lib/active_graphql.rb +10 -0
  24. data/lib/active_graphql/client.rb +38 -0
  25. data/lib/active_graphql/client/actions.rb +15 -0
  26. data/lib/active_graphql/client/actions/action.rb +116 -0
  27. data/lib/active_graphql/client/actions/action/format_inputs.rb +80 -0
  28. data/lib/active_graphql/client/actions/action/format_outputs.rb +40 -0
  29. data/lib/active_graphql/client/actions/mutation_action.rb +29 -0
  30. data/lib/active_graphql/client/actions/query_action.rb +23 -0
  31. data/lib/active_graphql/client/adapters.rb +10 -0
  32. data/lib/active_graphql/client/adapters/graphlient_adapter.rb +32 -0
  33. data/lib/active_graphql/client/response.rb +47 -0
  34. data/lib/active_graphql/errors.rb +11 -0
  35. data/lib/active_graphql/model.rb +174 -0
  36. data/lib/active_graphql/model/action_formatter.rb +96 -0
  37. data/lib/active_graphql/model/build_or_relation.rb +66 -0
  38. data/lib/active_graphql/model/configuration.rb +83 -0
  39. data/lib/active_graphql/model/find_in_batches.rb +54 -0
  40. data/lib/active_graphql/model/relation_proxy.rb +321 -0
  41. data/lib/active_graphql/version.rb +5 -0
  42. metadata +254 -0
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_graphql/version'
4
+
5
+ # nodoc
6
+ module ActiveGraphql
7
+ require 'active_support/core_ext/module/delegation'
8
+ require 'active_graphql/client'
9
+ require 'active_graphql/model'
10
+ end
@@ -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,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveGraphql
4
+ class Client
5
+ # nodoc
6
+ module Adapters
7
+ autoload :GraphlientAdapter, 'active_graphql/client/adapters/graphlient_adapter'
8
+ end
9
+ end
10
+ 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