graphiti_graphql 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -3
- data/Gemfile.lock +11 -15
- data/README.md +4 -2
- data/graphiti_graphql.gemspec +1 -1
- data/lib/graphiti_graphql/federation/schema_decorator.rb +27 -7
- data/lib/graphiti_graphql/graphiti_schema/resource.rb +4 -0
- data/lib/graphiti_graphql/runner.rb +27 -4
- data/lib/graphiti_graphql/schema.rb +68 -6
- data/lib/graphiti_graphql/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75a9873f8fda91fa24f41d06e146826f3de27165452b61202dc45d597e869ef5
|
4
|
+
data.tar.gz: 69b67b64c1af770a2df57a74e6a30175e7c80f4d26d4a73a2db958f153b1c5b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe62cbb5d5dd96ae3008add2bd13c465e0f2fd944869a478f544690bf6c459db2daf1504957adb243412a88cd318759fc6cd46f03a436b7b428d912d2530d2be
|
7
|
+
data.tar.gz: 2bd729f975995cf7433a3fc7c6864586dd5515955c68941c91e6036d2ef54cd024a3508a0869ed67eda7d6e61f612c37ae1b9b9b13ef19bb636948629edb4222
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,19 +1,9 @@
|
|
1
|
-
PATH
|
2
|
-
remote: ../graphiti
|
3
|
-
specs:
|
4
|
-
graphiti (1.2.32)
|
5
|
-
activesupport (>= 4.1)
|
6
|
-
concurrent-ruby (~> 1.0)
|
7
|
-
dry-types (>= 0.15.0, < 2.0)
|
8
|
-
graphiti_errors (~> 1.1.0)
|
9
|
-
jsonapi-renderer (~> 0.2, >= 0.2.2)
|
10
|
-
jsonapi-serializable (~> 0.3.0)
|
11
|
-
|
12
1
|
PATH
|
13
2
|
remote: .
|
14
3
|
specs:
|
15
|
-
graphiti_graphql (0.1.
|
4
|
+
graphiti_graphql (0.1.2)
|
16
5
|
activesupport (>= 4.1)
|
6
|
+
graphiti (>= 1.2.36)
|
17
7
|
graphql (~> 1.12)
|
18
8
|
|
19
9
|
GEM
|
@@ -54,6 +44,13 @@ GEM
|
|
54
44
|
dry-inflector (~> 0.1, >= 0.1.2)
|
55
45
|
dry-logic (~> 1.0, >= 1.0.2)
|
56
46
|
google-protobuf (3.15.2)
|
47
|
+
graphiti (1.2.36)
|
48
|
+
activesupport (>= 4.1)
|
49
|
+
concurrent-ruby (~> 1.0)
|
50
|
+
dry-types (>= 0.15.0, < 2.0)
|
51
|
+
graphiti_errors (~> 1.1.0)
|
52
|
+
jsonapi-renderer (~> 0.2, >= 0.2.2)
|
53
|
+
jsonapi-serializable (~> 0.3.0)
|
57
54
|
graphiti_errors (1.1.2)
|
58
55
|
jsonapi-serializable (~> 0.1)
|
59
56
|
graphiti_spec_helpers (1.0.5)
|
@@ -63,13 +60,13 @@ GEM
|
|
63
60
|
graphql-batch (0.4.3)
|
64
61
|
graphql (>= 1.3, < 2)
|
65
62
|
promise.rb (~> 0.7.2)
|
66
|
-
i18n (1.8.
|
63
|
+
i18n (1.8.9)
|
67
64
|
concurrent-ruby (~> 1.0)
|
68
65
|
jsonapi-renderer (0.2.2)
|
69
66
|
jsonapi-serializable (0.3.1)
|
70
67
|
jsonapi-renderer (~> 0.2.0)
|
71
68
|
method_source (1.0.0)
|
72
|
-
minitest (5.14.
|
69
|
+
minitest (5.14.4)
|
73
70
|
parallel (1.20.1)
|
74
71
|
parser (3.0.0.0)
|
75
72
|
ast (~> 2.4.1)
|
@@ -129,7 +126,6 @@ DEPENDENCIES
|
|
129
126
|
activemodel (>= 4.1)
|
130
127
|
apollo-federation (~> 1.1)
|
131
128
|
bundler (~> 2.0)
|
132
|
-
graphiti!
|
133
129
|
graphiti_graphql!
|
134
130
|
graphiti_spec_helpers
|
135
131
|
graphql (~> 1.12)
|
data/README.md
CHANGED
@@ -4,12 +4,14 @@ GraphQL (and Apollo Federation) support for Graphiti. Serve traditional Rails JS
|
|
4
4
|
|
5
5
|
Currently read-only, but you can add your own Mutations [manually](#blending-with-graphql-ruby).
|
6
6
|
|
7
|
+
Pre-alpha. Stay tuned.
|
8
|
+
|
7
9
|
## Setup
|
8
10
|
|
9
11
|
Add to your `Gemfile`:
|
10
12
|
|
11
13
|
```rb
|
12
|
-
gem 'graphiti', ">= 1.2.
|
14
|
+
gem 'graphiti', ">= 1.2.33"
|
13
15
|
gem "graphiti_graphql"
|
14
16
|
```
|
15
17
|
|
@@ -59,7 +61,7 @@ Define your Schema and Type classes as normal. Then in an initializer:
|
|
59
61
|
GraphitiGraphQL.schema_class = MySchema
|
60
62
|
```
|
61
63
|
|
62
|
-
Any pre-existing GraphQL endpoint will continue working as normal. But the GQL endpoint you mounted in `config/routes.rb` will now serve BOTH your low-level `graphql-ruby` schema AND your Graphiti-specific schema. Note these cannot (currently) be served side-by-side
|
64
|
+
Any pre-existing GraphQL endpoint will continue working as normal. But the GQL endpoint you mounted in `config/routes.rb` will now serve BOTH your low-level `graphql-ruby` schema AND your Graphiti-specific schema. Note these cannot (currently) be served side-by-side under `query` within the *same request*.
|
63
65
|
|
64
66
|
By default the GraphQL context will be `Graphiti.context[:object]`, which is the controller being called. You might want to customize this so your existing graphql-ruby code continues to expect the same context:
|
65
67
|
|
data/graphiti_graphql.gemspec
CHANGED
@@ -27,7 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
28
|
spec.require_paths = ["lib"]
|
29
29
|
|
30
|
-
spec.add_dependency "graphiti", ">= 1.2.
|
30
|
+
spec.add_dependency "graphiti", ">= 1.2.36"
|
31
31
|
spec.add_dependency "activesupport", ">= 4.1"
|
32
32
|
spec.add_dependency "graphql", "~> 1.12"
|
33
33
|
|
@@ -90,6 +90,22 @@ module GraphitiGraphQL
|
|
90
90
|
federated_type
|
91
91
|
end
|
92
92
|
|
93
|
+
def define_connection_type(name, type_class)
|
94
|
+
name = "#{name}FederatedConnection"
|
95
|
+
if (registered = type_registry[name])
|
96
|
+
return registered[:type]
|
97
|
+
end
|
98
|
+
|
99
|
+
klass = Class.new(@schema.class.base_object)
|
100
|
+
klass.graphql_name(name)
|
101
|
+
klass.field :nodes,
|
102
|
+
[type_class],
|
103
|
+
null: false,
|
104
|
+
extras: [:lookahead]
|
105
|
+
@schema.send :register, name, klass
|
106
|
+
klass
|
107
|
+
end
|
108
|
+
|
93
109
|
def define_federated_has_many(type_class, relationship)
|
94
110
|
local_name = GraphitiGraphQL::GraphitiSchema::Resource
|
95
111
|
.gql_name(relationship.local_resource_class.name)
|
@@ -100,16 +116,20 @@ module GraphitiGraphQL
|
|
100
116
|
local_interface = type_registry["I#{local_name}"]
|
101
117
|
best_type = local_interface ? local_interface[:type] : local_type
|
102
118
|
|
119
|
+
connection_type = define_connection_type(local_name, best_type)
|
120
|
+
|
103
121
|
field = type_class.field relationship.name,
|
104
|
-
|
122
|
+
connection_type,
|
105
123
|
null: false,
|
106
|
-
|
107
|
-
|
124
|
+
connection: false
|
108
125
|
@schema.send :define_arguments_for_sideload_field,
|
109
126
|
field, @schema.graphiti_schema.get_resource(local_resource_name)
|
110
|
-
|
111
|
-
|
112
|
-
|
127
|
+
|
128
|
+
type_class.define_method relationship.name do |**arguments|
|
129
|
+
{data: object, arguments: arguments}
|
130
|
+
end
|
131
|
+
connection_type.define_method :nodes do |lookahead:, **arguments|
|
132
|
+
params = object[:arguments].as_json
|
113
133
|
.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
114
134
|
selections = lookahead.selections.map(&:name)
|
115
135
|
selections << relationship.foreign_key
|
@@ -122,7 +142,7 @@ module GraphitiGraphQL
|
|
122
142
|
|
123
143
|
Federation::Loaders::HasMany
|
124
144
|
.for(relationship, params)
|
125
|
-
.load(object[:id])
|
145
|
+
.load(object[:data][:id])
|
126
146
|
end
|
127
147
|
end
|
128
148
|
|
@@ -38,7 +38,7 @@ module GraphitiGraphQL
|
|
38
38
|
|
39
39
|
def render(json, selection_name)
|
40
40
|
payload = if find_one?(selection_name)
|
41
|
-
{selection_name.to_sym => json.values[0][0]}
|
41
|
+
{selection_name.to_sym => json.values[0][:nodes][0]}
|
42
42
|
else
|
43
43
|
json
|
44
44
|
end
|
@@ -108,15 +108,19 @@ module GraphitiGraphQL
|
|
108
108
|
gather_filters(params, selection, variables_hash, chained_name)
|
109
109
|
gather_sorts(params, selection, variables_hash, chained_name)
|
110
110
|
gather_pages(params, selection, variables_hash, chained_name)
|
111
|
+
gather_stats(params, selection, variables_hash, chained_name)
|
111
112
|
end
|
112
113
|
|
113
114
|
params[:include] ||= []
|
114
115
|
params[:include] << chained_name if chained_name
|
115
116
|
|
116
|
-
|
117
|
+
nodes = selection.selections.find { |s| s.respond_to?(:name) && s.name == "nodes" }
|
118
|
+
children = nodes ? nodes.children : selection.selections
|
119
|
+
|
120
|
+
fragments = children.select { |s|
|
117
121
|
s.is_a?(GraphQL::Language::Nodes::InlineFragment)
|
118
122
|
}
|
119
|
-
non_fragments =
|
123
|
+
non_fragments = children - fragments
|
120
124
|
|
121
125
|
if pbt
|
122
126
|
# Only id/_type possible here
|
@@ -223,7 +227,9 @@ module GraphitiGraphQL
|
|
223
227
|
|
224
228
|
attr_arg.value.arguments.each do |operator_arg|
|
225
229
|
value = operator_arg.value
|
226
|
-
if value.
|
230
|
+
if value.is_a?(GraphQL::Language::Nodes::Enum)
|
231
|
+
value = value.name
|
232
|
+
elsif value.respond_to?(:name) # is a variable
|
227
233
|
value = variable_hash[operator_arg.value.name]
|
228
234
|
end
|
229
235
|
f[filter_param_name] = {operator_arg.name.underscore => value}
|
@@ -299,5 +305,22 @@ module GraphitiGraphQL
|
|
299
305
|
params[:page].merge!(pages)
|
300
306
|
end
|
301
307
|
end
|
308
|
+
|
309
|
+
def gather_stats(params, selection, variable_hash, chained_name = nil)
|
310
|
+
stats = selection.children.find { |c| c.name == "stats" }
|
311
|
+
nodes = selection.children.find { |c| c.name == "nodes" }
|
312
|
+
|
313
|
+
if stats
|
314
|
+
stat_param = {}
|
315
|
+
stats.children.each do |stat_node|
|
316
|
+
stat_name = stat_node.name
|
317
|
+
calculations = stat_node.children.map(&:name)
|
318
|
+
stat_param[stat_name.to_sym] = calculations.join(",")
|
319
|
+
end
|
320
|
+
stat_param = {chained_name => stat_param} if chained_name
|
321
|
+
params[:stats] = stat_param
|
322
|
+
params[:page] = {size: 0} unless nodes
|
323
|
+
end
|
324
|
+
end
|
302
325
|
end
|
303
326
|
end
|
@@ -132,11 +132,30 @@ module GraphitiGraphQL
|
|
132
132
|
|
133
133
|
private
|
134
134
|
|
135
|
+
def generate_connection_type(resource, top_level: true)
|
136
|
+
name = "#{resource.graphql_class_name}#{top_level ? "TopLevel" : ""}Connection"
|
137
|
+
if registered = type_registry[name]
|
138
|
+
return registered[:type]
|
139
|
+
end
|
140
|
+
|
141
|
+
type = type_registry[resource.graphql_class_name][:type]
|
142
|
+
klass = Class.new(self.class.base_object)
|
143
|
+
klass.graphql_name(name)
|
144
|
+
klass.field :nodes,
|
145
|
+
[type],
|
146
|
+
"List #{resource.graphql_class_name(false).pluralize}",
|
147
|
+
null: false
|
148
|
+
if top_level
|
149
|
+
klass.field :stats, generate_stat_class(resource), null: false
|
150
|
+
end
|
151
|
+
register(name, klass)
|
152
|
+
klass
|
153
|
+
end
|
154
|
+
|
135
155
|
def add_index(query_class, resource)
|
136
156
|
field_name = resource.graphql_entrypoint.to_s.underscore.to_sym
|
137
157
|
field = query_class.field field_name,
|
138
|
-
|
139
|
-
"List #{resource.graphql_class_name(false).pluralize}",
|
158
|
+
generate_connection_type(resource, top_level: true),
|
140
159
|
null: false
|
141
160
|
@query_fields[field_name] = resource
|
142
161
|
define_arguments_for_sideload_field(field, resource)
|
@@ -147,7 +166,7 @@ module GraphitiGraphQL
|
|
147
166
|
field = query_class.field field_name,
|
148
167
|
type_registry[resource.graphql_class_name][:type],
|
149
168
|
"Single #{resource.graphql_class_name(false).singularize}",
|
150
|
-
null:
|
169
|
+
null: false
|
151
170
|
@query_fields[field_name] = resource
|
152
171
|
define_arguments_for_sideload_field field,
|
153
172
|
resource,
|
@@ -232,17 +251,38 @@ module GraphitiGraphQL
|
|
232
251
|
# TODO guarded operators or otherwise whatever eq => nil is
|
233
252
|
def generate_filter_attribute_type(type_name, filter_name, filter_config)
|
234
253
|
klass = Class.new(GraphQL::Schema::InputObject)
|
235
|
-
|
254
|
+
filter_graphql_name = "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
|
255
|
+
klass.graphql_name(filter_graphql_name)
|
236
256
|
filter_config[:operators].each do |operator|
|
237
257
|
canonical_graphiti_type = Graphiti::Types
|
238
258
|
.name_for(filter_config[:type])
|
239
259
|
type = GQL_TYPE_MAP[canonical_graphiti_type]
|
240
260
|
required = !!filter_config[:required] && operator == "eq"
|
261
|
+
|
262
|
+
if (allowlist = filter_config[:allow])
|
263
|
+
type = define_allowlist_type(filter_graphql_name, allowlist)
|
264
|
+
end
|
265
|
+
|
266
|
+
type = [type] unless !!filter_config[:single]
|
241
267
|
klass.argument operator, type, required: required
|
242
268
|
end
|
243
269
|
klass
|
244
270
|
end
|
245
271
|
|
272
|
+
def define_allowlist_type(filter_graphql_name, allowlist)
|
273
|
+
name = "#{filter_graphql_name}Allow"
|
274
|
+
if (registered = type_registry[name])
|
275
|
+
return registered[:type]
|
276
|
+
end
|
277
|
+
klass = Class.new(GraphQL::Schema::Enum)
|
278
|
+
klass.graphql_name(name)
|
279
|
+
allowlist.each do |allowed|
|
280
|
+
klass.value(allowed)
|
281
|
+
end
|
282
|
+
register(name, klass)
|
283
|
+
klass
|
284
|
+
end
|
285
|
+
|
246
286
|
def generate_resource_for_sideload(sideload)
|
247
287
|
if sideload.type == :polymorphic_belongs_to
|
248
288
|
unless registered?(sideload.parent_resource)
|
@@ -269,7 +309,11 @@ module GraphitiGraphQL
|
|
269
309
|
type_registry[sideload.graphql_class_name][:type]
|
270
310
|
end
|
271
311
|
|
272
|
-
gql_field_type = sideload.to_many?
|
312
|
+
gql_field_type = if sideload.to_many?
|
313
|
+
generate_connection_type(sideload.resource, top_level: false)
|
314
|
+
else
|
315
|
+
gql_type
|
316
|
+
end
|
273
317
|
field_name = name.to_s.camelize(:lower)
|
274
318
|
unless type_class.fields[field_name]
|
275
319
|
field = type_class.field field_name.to_sym,
|
@@ -277,7 +321,6 @@ module GraphitiGraphQL
|
|
277
321
|
null: !sideload.to_many?
|
278
322
|
|
279
323
|
# No sort/filter/paginate on belongs_to
|
280
|
-
# unless sideload.type.to_s.include?('belongs_to')
|
281
324
|
unless sideload.type == :polymorphic_belongs_to
|
282
325
|
define_arguments_for_sideload_field(field, sideload.resource)
|
283
326
|
end
|
@@ -373,6 +416,25 @@ module GraphitiGraphQL
|
|
373
416
|
klass
|
374
417
|
end
|
375
418
|
|
419
|
+
def generate_stat_class(resource)
|
420
|
+
klass = Class.new(self.class.base_object)
|
421
|
+
klass.graphql_name "#{resource.graphql_class_name(false)}Stats"
|
422
|
+
resource.stats.each_pair do |name, calculations|
|
423
|
+
calc_class = generate_calc_class(resource, name, calculations)
|
424
|
+
klass.field name, calc_class, null: false
|
425
|
+
end
|
426
|
+
klass
|
427
|
+
end
|
428
|
+
|
429
|
+
def generate_calc_class(resource, stat_name, calculations)
|
430
|
+
klass = Class.new(self.class.base_object)
|
431
|
+
klass.graphql_name "#{resource.graphql_class_name(false)}#{stat_name}Calculations"
|
432
|
+
calculations.each do |calc|
|
433
|
+
klass.field calc, Float, null: false
|
434
|
+
end
|
435
|
+
klass
|
436
|
+
end
|
437
|
+
|
376
438
|
def registered?(resource)
|
377
439
|
name = resource.graphql_class_name(false)
|
378
440
|
!!type_registry[name]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: graphiti_graphql
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lee Richmond
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: graphiti
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: 1.2.
|
19
|
+
version: 1.2.36
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: 1.2.
|
26
|
+
version: 1.2.36
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: activesupport
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|