graphql-client 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/graphql/client.rb +42 -11
- data/lib/graphql/client/collocated_enforcement.rb +51 -0
- data/lib/graphql/client/query_result.rb +48 -7
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88afea7eaa70819f11b2a000886ef3924e2b0e12
|
4
|
+
data.tar.gz: 62a8706efe7fe20a73da2affaa3e362dce686970
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b9774d7f5229a0202924ff253b251076ed7b5f71dd3e32f0707467663868faace9a331b79878e70159058ebbdde59b477892b13c662c1e3f9492fa31e6901551
|
7
|
+
data.tar.gz: 0d4e18520e7b23a28012edf77a98913358ef7793e70f46ddbdf881ad63c51eafc83325ef7a388d37b752914d55b0f6e9d23dc50efbb23afc98bf1f5aebd25be0
|
data/lib/graphql/client.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "active_support/inflector"
|
3
3
|
require "active_support/notifications"
|
4
4
|
require "graphql"
|
5
|
+
require "graphql/client/collocated_enforcement"
|
5
6
|
require "graphql/client/error"
|
6
7
|
require "graphql/client/errors"
|
7
8
|
require "graphql/client/query_result"
|
@@ -22,10 +23,15 @@ module GraphQL
|
|
22
23
|
class NotImplementedError < Error; end
|
23
24
|
class ValidationError < Error; end
|
24
25
|
|
26
|
+
extend CollocatedEnforcement
|
27
|
+
|
25
28
|
attr_reader :schema, :execute
|
26
29
|
|
27
30
|
attr_accessor :document_tracking_enabled
|
28
31
|
|
32
|
+
# Public: Check if collocated caller enforcement is enabled.
|
33
|
+
attr_reader :enforce_collocated_callers
|
34
|
+
|
29
35
|
# Deprecated: Allow dynamically generated queries to be passed to
|
30
36
|
# Client#query.
|
31
37
|
#
|
@@ -72,12 +78,13 @@ module GraphQL
|
|
72
78
|
result
|
73
79
|
end
|
74
80
|
|
75
|
-
def initialize(schema: nil, execute: nil)
|
81
|
+
def initialize(schema: nil, execute: nil, enforce_collocated_callers: false)
|
76
82
|
@schema = self.class.load_schema(schema)
|
77
83
|
@execute = execute
|
78
84
|
@document = GraphQL::Language::Nodes::Document.new(definitions: [])
|
79
85
|
@document_tracking_enabled = false
|
80
86
|
@allow_dynamic_queries = false
|
87
|
+
@enforce_collocated_callers = enforce_collocated_callers
|
81
88
|
end
|
82
89
|
|
83
90
|
# Definitions are constructed by Client.parse and wrap a parsed AST of the
|
@@ -97,11 +104,13 @@ module GraphQL
|
|
97
104
|
end
|
98
105
|
end
|
99
106
|
|
100
|
-
def initialize(node:, document:, schema:, document_types:)
|
107
|
+
def initialize(node:, document:, schema:, document_types:, source_location:, enforce_collocated_callers:)
|
101
108
|
@definition_node = node
|
102
109
|
@document = document
|
103
110
|
@schema = schema
|
104
111
|
@document_types = document_types
|
112
|
+
@source_location = source_location
|
113
|
+
@enforce_collocated_callers = enforce_collocated_callers
|
105
114
|
end
|
106
115
|
|
107
116
|
# Internal: Get underlying operation or fragment defintion AST node for
|
@@ -134,15 +143,26 @@ module GraphQL
|
|
134
143
|
# and any FragmentDefinition dependencies.
|
135
144
|
attr_reader :document
|
136
145
|
|
146
|
+
# Internal: Mapping of document nodes to schema types.
|
147
|
+
attr_reader :document_types
|
148
|
+
|
137
149
|
attr_reader :schema
|
138
150
|
|
151
|
+
# Public: Returns the Ruby source filename and line number containing this
|
152
|
+
# definition was not defined in Ruby.
|
153
|
+
#
|
154
|
+
# Returns Array pair of [String, Fixnum].
|
155
|
+
attr_reader :source_location
|
156
|
+
|
157
|
+
attr_reader :enforce_collocated_callers
|
158
|
+
|
139
159
|
def new(*args)
|
140
160
|
type.new(*args)
|
141
161
|
end
|
142
162
|
|
143
163
|
def type
|
144
164
|
# TODO: Fix type indirection
|
145
|
-
@type ||= GraphQL::Client::QueryResult.wrap(definition_node, name: "#{name}.type"
|
165
|
+
@type ||= GraphQL::Client::QueryResult.wrap(self, definition_node, name: "#{name}.type")
|
146
166
|
end
|
147
167
|
end
|
148
168
|
|
@@ -158,11 +178,22 @@ module GraphQL
|
|
158
178
|
end
|
159
179
|
|
160
180
|
def parse(str, filename = nil, lineno = nil)
|
161
|
-
if filename.nil?
|
162
|
-
|
163
|
-
|
181
|
+
if filename.nil? && lineno.nil?
|
182
|
+
location = caller_locations(1, 1).first
|
183
|
+
filename = location.path
|
184
|
+
lineno = location.lineno
|
164
185
|
end
|
165
186
|
|
187
|
+
unless filename.is_a?(String)
|
188
|
+
raise TypeError, "expected filename to be a String, but was #{filename.class}"
|
189
|
+
end
|
190
|
+
|
191
|
+
unless lineno.is_a?(Integer)
|
192
|
+
raise TypeError, "expected lineno to be a Integer, but was #{lineno.class}"
|
193
|
+
end
|
194
|
+
|
195
|
+
source_location = [filename, lineno].freeze
|
196
|
+
|
166
197
|
definition_dependencies = Set.new
|
167
198
|
|
168
199
|
str = str.gsub(/\.\.\.([a-zA-Z0-9_]+(::[a-zA-Z0-9_]+)+)/) do
|
@@ -186,9 +217,7 @@ module GraphQL
|
|
186
217
|
end
|
187
218
|
|
188
219
|
error = ValidationError.new(message)
|
189
|
-
|
190
|
-
error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller)
|
191
|
-
end
|
220
|
+
error.set_backtrace(["#{filename}:#{lineno + match.pre_match.count("\n") + 1}"] + caller)
|
192
221
|
raise error
|
193
222
|
end
|
194
223
|
end
|
@@ -211,7 +240,7 @@ module GraphQL
|
|
211
240
|
error_hash = error.to_h
|
212
241
|
validation_line = error_hash["locations"][0]["line"]
|
213
242
|
error = ValidationError.new(error_hash["message"])
|
214
|
-
error.set_backtrace(["#{filename}:#{lineno + validation_line}"] + caller)
|
243
|
+
error.set_backtrace(["#{filename}:#{lineno + validation_line}"] + caller)
|
215
244
|
raise error
|
216
245
|
end
|
217
246
|
|
@@ -230,7 +259,9 @@ module GraphQL
|
|
230
259
|
schema: @schema,
|
231
260
|
node: node,
|
232
261
|
document: sliced_document,
|
233
|
-
document_types: document_types
|
262
|
+
document_types: document_types,
|
263
|
+
source_location: source_location,
|
264
|
+
enforce_collocated_callers: enforce_collocated_callers
|
234
265
|
)
|
235
266
|
definitions[node.name] = definition
|
236
267
|
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "graphql/client/error"
|
3
|
+
|
4
|
+
module GraphQL
|
5
|
+
class Client
|
6
|
+
# Raised when method is called from outside the expected file scope.
|
7
|
+
class NonCollocatedCallerError < Error; end
|
8
|
+
|
9
|
+
# Enforcements collocated object access best practices.
|
10
|
+
module CollocatedEnforcement
|
11
|
+
# Public: Ignore collocated caller enforcement for the scope of the block.
|
12
|
+
def allow_noncollocated_callers
|
13
|
+
Thread.current[:query_result_caller_location_ignore] = true
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
Thread.current[:query_result_caller_location_ignore] = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# Internal: Decorate method with collocated caller enforcement.
|
20
|
+
#
|
21
|
+
# mod - Target Module/Class
|
22
|
+
# methods - Array of Symbol method names
|
23
|
+
# path - String filename to assert calling from
|
24
|
+
#
|
25
|
+
# Returns nothing.
|
26
|
+
def enforce_collocated_callers(mod, methods, path)
|
27
|
+
mod.prepend(Module.new do
|
28
|
+
methods.each do |method|
|
29
|
+
define_method(method) do |*args, &block|
|
30
|
+
return super(*args, &block) if Thread.current[:query_result_caller_location_ignore]
|
31
|
+
|
32
|
+
locations = caller_locations(1)
|
33
|
+
if locations.first.path != path
|
34
|
+
error = NonCollocatedCallerError.new("#{method} was called outside of '#{path}' https://git.io/v1syX")
|
35
|
+
error.set_backtrace(locations.map(&:to_s))
|
36
|
+
raise error
|
37
|
+
end
|
38
|
+
|
39
|
+
begin
|
40
|
+
Thread.current[:query_result_caller_location_ignore] = true
|
41
|
+
super(*args, &block)
|
42
|
+
ensure
|
43
|
+
Thread.current[:query_result_caller_location_ignore] = nil
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -17,7 +17,7 @@ module GraphQL
|
|
17
17
|
# Internal: Get QueryResult class for result of query.
|
18
18
|
#
|
19
19
|
# Returns subclass of QueryResult or nil.
|
20
|
-
def self.wrap(node, name: nil
|
20
|
+
def self.wrap(source_definition, node, name: nil)
|
21
21
|
fields = {}
|
22
22
|
|
23
23
|
node.selections.each do |selection|
|
@@ -27,40 +27,74 @@ module GraphQL
|
|
27
27
|
when Language::Nodes::Field
|
28
28
|
field_name = selection.alias || selection.name
|
29
29
|
field_klass = nil
|
30
|
-
|
30
|
+
if selection.selections.any?
|
31
|
+
field_klass = wrap(source_definition, selection, name: "#{name}[:#{field_name}]")
|
32
|
+
end
|
31
33
|
fields[field_name] ? fields[field_name] |= field_klass : fields[field_name] = field_klass
|
32
34
|
when Language::Nodes::InlineFragment
|
33
|
-
wrap(selection, name: name
|
35
|
+
wrap(source_definition, selection, name: name).fields.each do |fragment_name, klass|
|
34
36
|
fields[fragment_name.to_s] ? fields[fragment_name.to_s] |= klass : fields[fragment_name.to_s] = klass
|
35
37
|
end
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
define(name: name, source_node: node, fields: fields
|
41
|
+
define(name: name, source_definition: source_definition, source_node: node, fields: fields)
|
42
|
+
end
|
43
|
+
|
44
|
+
# :nodoc:
|
45
|
+
class Scalar
|
46
|
+
def initialize(type)
|
47
|
+
@type = type
|
48
|
+
end
|
49
|
+
|
50
|
+
def cast(value, _errors = nil)
|
51
|
+
@type.coerce_input(value)
|
52
|
+
end
|
53
|
+
|
54
|
+
def |(_other)
|
55
|
+
# XXX: How would scalars merge?
|
56
|
+
self
|
57
|
+
end
|
40
58
|
end
|
41
59
|
|
42
60
|
# Internal
|
43
|
-
def self.define(name:, source_node:, fields: {}
|
61
|
+
def self.define(name:, source_definition:, source_node:, fields: {})
|
62
|
+
type = source_definition.document_types[source_node]
|
63
|
+
type = type.unwrap if type
|
64
|
+
|
44
65
|
Class.new(self) do
|
45
66
|
@name = name
|
46
67
|
@type = type
|
47
68
|
@source_node = source_node
|
69
|
+
@source_definition = source_definition
|
48
70
|
@fields = {}
|
49
71
|
|
72
|
+
field_readers = Set.new
|
73
|
+
|
50
74
|
fields.each do |field, klass|
|
75
|
+
if @type.is_a?(GraphQL::ObjectType)
|
76
|
+
field_node = @type.fields[field.to_s]
|
77
|
+
if field_node && field_node.type.unwrap.is_a?(GraphQL::ScalarType)
|
78
|
+
klass = Scalar.new(field_node.type.unwrap)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
51
82
|
@fields[field.to_sym] = klass
|
52
83
|
|
53
84
|
send :attr_reader, field
|
85
|
+
field_readers << field.to_sym
|
54
86
|
|
55
87
|
# Convert GraphQL camelcase to snake case: commitComments -> commit_comments
|
56
88
|
field_alias = ActiveSupport::Inflector.underscore(field)
|
57
89
|
send :alias_method, field_alias, field if field != field_alias
|
90
|
+
field_readers << field_alias.to_sym
|
58
91
|
|
59
92
|
class_eval <<-RUBY, __FILE__, __LINE__
|
60
93
|
def #{field_alias}?
|
61
94
|
#{field_alias} ? true : false
|
62
95
|
end
|
63
96
|
RUBY
|
97
|
+
field_readers << "#{field_alias}?".to_sym
|
64
98
|
|
65
99
|
next unless field == "edges"
|
66
100
|
class_eval <<-RUBY, __FILE__, __LINE__
|
@@ -70,9 +104,10 @@ module GraphQL
|
|
70
104
|
self
|
71
105
|
end
|
72
106
|
RUBY
|
107
|
+
field_readers << :each_node
|
73
108
|
end
|
74
109
|
|
75
|
-
assigns = fields.map do |field, klass|
|
110
|
+
assigns = @fields.map do |field, klass|
|
76
111
|
if klass
|
77
112
|
<<-RUBY
|
78
113
|
@#{field} = self.class.fields[:#{field}].cast(@data["#{field}"], @errors.filter_by_path("#{field}"))
|
@@ -97,12 +132,18 @@ module GraphQL
|
|
97
132
|
freeze
|
98
133
|
end
|
99
134
|
RUBY
|
135
|
+
|
136
|
+
if @source_definition.enforce_collocated_callers
|
137
|
+
Client.enforce_collocated_callers(self, field_readers, source_definition.source_location[0])
|
138
|
+
end
|
100
139
|
end
|
101
140
|
end
|
102
141
|
|
103
142
|
class << self
|
104
143
|
attr_reader :type
|
105
144
|
|
145
|
+
attr_reader :source_definition
|
146
|
+
|
106
147
|
attr_reader :source_node
|
107
148
|
|
108
149
|
attr_reader :fields
|
@@ -175,7 +216,7 @@ module GraphQL
|
|
175
216
|
end
|
176
217
|
end
|
177
218
|
# TODO: Picking first source node seems error prone
|
178
|
-
define(name: self.name, source_node: source_node, fields: new_fields)
|
219
|
+
define(name: self.name, source_definition: source_definition, source_node: source_node, fields: new_fields)
|
179
220
|
end
|
180
221
|
|
181
222
|
# Public: Return errors associated with data.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphql-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- GitHub
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-12-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -34,16 +34,16 @@ dependencies:
|
|
34
34
|
name: graphql
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
36
36
|
requirements:
|
37
|
-
- - "
|
37
|
+
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: '1.2'
|
40
40
|
type: :runtime
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
|
-
- - "
|
44
|
+
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: '1.2'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: actionpack
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,6 +115,7 @@ files:
|
|
115
115
|
- LICENSE
|
116
116
|
- README.md
|
117
117
|
- lib/graphql/client.rb
|
118
|
+
- lib/graphql/client/collocated_enforcement.rb
|
118
119
|
- lib/graphql/client/document_types.rb
|
119
120
|
- lib/graphql/client/error.rb
|
120
121
|
- lib/graphql/client/errors.rb
|