graphiti_graphql 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +4 -4
- data/README.md +139 -15
- data/graphiti_graphql.gemspec +1 -0
- data/lib/graphiti_graphql.rb +47 -34
- data/lib/graphiti_graphql/engine.rb +2 -12
- data/lib/graphiti_graphql/federation.rb +12 -220
- data/lib/graphiti_graphql/federation/apollo_federation_override.rb +54 -0
- data/lib/graphiti_graphql/federation/federated_relationship.rb +26 -0
- data/lib/graphiti_graphql/federation/federated_resource.rb +26 -0
- data/lib/graphiti_graphql/federation/loaders/belongs_to.rb +22 -0
- data/lib/graphiti_graphql/federation/loaders/has_many.rb +33 -0
- data/lib/graphiti_graphql/federation/resource_dsl.rb +89 -0
- data/lib/graphiti_graphql/federation/schema_decorator.rb +150 -0
- data/lib/graphiti_graphql/graphiti_schema/resource.rb +0 -18
- data/lib/graphiti_graphql/graphiti_schema/wrapper.rb +0 -15
- data/lib/graphiti_graphql/runner.rb +15 -25
- data/lib/graphiti_graphql/schema.rb +50 -160
- data/lib/graphiti_graphql/util.rb +1 -1
- data/lib/graphiti_graphql/version.rb +1 -1
- metadata +23 -2
@@ -4,22 +4,28 @@ module GraphitiGraphQL
|
|
4
4
|
query = GraphQL::Query.new(schema, query_string, variables: variables)
|
5
5
|
definition = query.document.definitions.first
|
6
6
|
selection = definition.selections.first
|
7
|
-
|
7
|
+
is_graphiti = schemas.generated.query_field?(selection.name)
|
8
8
|
|
9
|
-
#
|
9
|
+
# Wrap *everything* in context, in case of federated request
|
10
10
|
Util.with_gql_context do
|
11
|
-
if
|
11
|
+
if is_graphiti
|
12
|
+
resource_class = schemas.generated
|
13
|
+
.resource_for_query_field(selection.name)
|
12
14
|
run_query(schema, resource_class, selection, query)
|
13
15
|
else
|
14
|
-
|
16
|
+
schemas.graphql.execute query_string,
|
15
17
|
variables: variables,
|
16
|
-
context: GraphitiGraphQL.get_context
|
18
|
+
context: GraphitiGraphQL.config.get_context
|
17
19
|
end
|
18
20
|
end
|
19
21
|
end
|
20
22
|
|
21
23
|
private
|
22
24
|
|
25
|
+
def schemas
|
26
|
+
GraphitiGraphQL.schemas
|
27
|
+
end
|
28
|
+
|
23
29
|
def run_query(schema, resource_class, selection, query)
|
24
30
|
if (errors = collect_errors(schema, query)).any?
|
25
31
|
{"errors" => errors.map(&:to_h)}
|
@@ -49,20 +55,8 @@ module GraphitiGraphQL
|
|
49
55
|
query.validation_errors + query.analysis_errors + query.context.errors
|
50
56
|
end
|
51
57
|
|
52
|
-
# We can't just constantize the name from the schema
|
53
|
-
# Because classes can be reopened and modified in tests (or elsewhere, in theory)
|
54
|
-
def find_entrypoint_resource_class(entrypoint)
|
55
|
-
Graphiti.resources.find(&matches_entrypoint?(entrypoint))
|
56
|
-
end
|
57
|
-
|
58
58
|
def find_entrypoint_schema_resource(entrypoint)
|
59
|
-
|
60
|
-
end
|
61
|
-
|
62
|
-
def matches_entrypoint?(entrypoint)
|
63
|
-
lambda do |resource|
|
64
|
-
resource.graphql_entrypoint.to_s.underscore == entrypoint.pluralize.underscore
|
65
|
-
end
|
59
|
+
schemas.generated.schema_resource_for_query_field(entrypoint)
|
66
60
|
end
|
67
61
|
|
68
62
|
def introspection_query?(query)
|
@@ -70,14 +64,10 @@ module GraphitiGraphQL
|
|
70
64
|
end
|
71
65
|
|
72
66
|
def find_resource_by_selection_name(name)
|
73
|
-
|
67
|
+
schemas.graphiti.resources
|
74
68
|
.find { |r| r.type == name.pluralize.underscore }
|
75
69
|
end
|
76
70
|
|
77
|
-
def graphiti_schema
|
78
|
-
Graphiti.graphql_schema.graphiti_schema
|
79
|
-
end
|
80
|
-
|
81
71
|
def schema_resource_for_selection(selection, parent_resource)
|
82
72
|
if parent_resource
|
83
73
|
parent_resource.related_resource(selection.name.underscore.to_sym)
|
@@ -146,8 +136,8 @@ module GraphitiGraphQL
|
|
146
136
|
end
|
147
137
|
|
148
138
|
fragments.each do |fragment|
|
149
|
-
resource_name =
|
150
|
-
klass =
|
139
|
+
resource_name = schemas.generated.type_registry[fragment.type.name][:resource]
|
140
|
+
klass = schemas.graphiti.resources.find { |r| r.name == resource_name }
|
151
141
|
_, _, fragment_sideload_selections = gather_fields fragment.selections,
|
152
142
|
klass,
|
153
143
|
params,
|
@@ -34,6 +34,7 @@ module GraphitiGraphQL
|
|
34
34
|
end
|
35
35
|
|
36
36
|
attr_accessor :type_registry, :schema, :graphiti_schema
|
37
|
+
attr_reader :query_fields
|
37
38
|
|
38
39
|
def self.federation?
|
39
40
|
!!@federation
|
@@ -54,26 +55,11 @@ module GraphitiGraphQL
|
|
54
55
|
def self.generate(entrypoint_resources = nil)
|
55
56
|
instance = new
|
56
57
|
schema = Class.new(::GraphitiGraphQL.schema_class || GraphQL::Schema)
|
57
|
-
|
58
|
-
if federation?
|
59
|
-
schema.send(:include, ApolloFederation::Schema)
|
60
|
-
end
|
61
|
-
|
62
58
|
graphiti_schema = GraphitiGraphQL::GraphitiSchema::Wrapper
|
63
59
|
.new(Graphiti::Schema.generate)
|
64
|
-
# TODO: if we avoid this on federation, or remove altogether
|
65
|
-
# Make sure we don't blow up
|
66
|
-
# graphiti_schema.merge_remotes!
|
67
|
-
|
68
|
-
entries = entrypoint_resources || entrypoints
|
69
|
-
instance.apply_query(graphiti_schema, schema, entries)
|
70
|
-
|
71
|
-
# NB if we add mutation support, make sure this is applied after
|
72
|
-
if federation?
|
73
|
-
schema.use GraphQL::Batch
|
74
|
-
end
|
75
|
-
instance.schema = schema
|
76
60
|
instance.graphiti_schema = graphiti_schema
|
61
|
+
instance.schema = schema
|
62
|
+
instance.apply_query(entrypoint_resources || entrypoints)
|
77
63
|
instance
|
78
64
|
end
|
79
65
|
|
@@ -83,147 +69,32 @@ module GraphitiGraphQL
|
|
83
69
|
|
84
70
|
def initialize
|
85
71
|
@type_registry = {}
|
72
|
+
@query_fields = {}
|
86
73
|
end
|
87
74
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
def apply_federation(graphiti_schema, graphql_schema)
|
92
|
-
type_registry.each_pair do |name, config|
|
93
|
-
if config[:resource]
|
94
|
-
local_type = config[:type]
|
95
|
-
local_resource = Graphiti.resources
|
96
|
-
.find { |r| r.name == config[:resource] }
|
97
|
-
# TODO: maybe turn off the graphiti debug for these?
|
98
|
-
local_type.define_singleton_method :resolve_reference do |reference, context, lookahead|
|
99
|
-
Federation::BelongsToLoader
|
100
|
-
.for(local_resource, lookahead.selections.map(&:name))
|
101
|
-
.load(reference[:id])
|
102
|
-
end
|
103
|
-
end
|
104
|
-
end
|
105
|
-
|
106
|
-
# NB: test already registered bc 2 things have same relationship
|
107
|
-
GraphitiGraphQL::Federation.external_resources.each_pair do |klass_name, config|
|
108
|
-
pre_registered = !!type_registry[klass_name]
|
109
|
-
external_klass = if pre_registered
|
110
|
-
type_registry[klass_name][:type]
|
111
|
-
else
|
112
|
-
external_klass = Class.new(self.class.base_object)
|
113
|
-
external_klass.graphql_name klass_name
|
114
|
-
external_klass
|
115
|
-
end
|
116
|
-
|
117
|
-
unless pre_registered
|
118
|
-
external_klass.key(fields: "id")
|
119
|
-
external_klass.extend_type
|
120
|
-
external_klass.field :id, String, null: false, external: true
|
121
|
-
external_klass.class_eval do
|
122
|
-
def self.resolve_reference(reference, _context, _lookup)
|
123
|
-
reference
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
unless pre_registered
|
129
|
-
# NB must be registered before processing rels
|
130
|
-
type_registry[klass_name] = { type: external_klass }
|
131
|
-
end
|
132
|
-
|
133
|
-
# TODO: only do it if field not already defined
|
134
|
-
config.relationships.each_pair do |name, relationship|
|
135
|
-
if relationship.has_many?
|
136
|
-
define_federated_has_many(graphiti_schema, external_klass, relationship)
|
137
|
-
elsif relationship.belongs_to?
|
138
|
-
define_federated_belongs_to(config, relationship)
|
139
|
-
end
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
# TODO: refactor to not constantly pass schemas around
|
145
|
-
def define_federated_has_many(graphiti_schema, external_klass, relationship)
|
146
|
-
local_name = GraphitiGraphQL::GraphitiSchema::Resource
|
147
|
-
.gql_name(relationship.local_resource_class.name)
|
148
|
-
local_type = type_registry[local_name][:type]
|
149
|
-
local_resource_name = type_registry[local_name][:resource]
|
150
|
-
local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
|
151
|
-
|
152
|
-
local_interface = type_registry["I#{local_name}"]
|
153
|
-
best_type = local_interface ? local_interface[:type] : local_type
|
154
|
-
|
155
|
-
field = external_klass.field relationship.name,
|
156
|
-
[best_type],
|
157
|
-
null: false,
|
158
|
-
extras: [:lookahead]
|
159
|
-
|
160
|
-
define_arguments_for_sideload_field(field, graphiti_schema.get_resource(local_resource_name))
|
161
|
-
external_klass.define_method relationship.name do |lookahead:, **arguments|
|
162
|
-
# TODO test params...do version of sort with array/symbol keys and plain string
|
163
|
-
params = arguments.as_json
|
164
|
-
.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
165
|
-
selections = lookahead.selections.map(&:name)
|
166
|
-
selections << relationship.foreign_key
|
167
|
-
selections << :_type # polymorphism
|
168
|
-
params[:fields] = { local_resource.type => selections.join(",") }
|
169
|
-
|
170
|
-
if (sort = Util.parse_sort(params[:sort]))
|
171
|
-
params[:sort] = sort
|
172
|
-
end
|
173
|
-
|
174
|
-
Federation::HasManyLoader
|
175
|
-
.for(local_resource, params, relationship.foreign_key)
|
176
|
-
.load(object[:id])
|
177
|
-
end
|
178
|
-
end
|
179
|
-
|
180
|
-
def define_federated_belongs_to(external_resource_config, relationship)
|
181
|
-
type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
|
182
|
-
local_type = type_registry[type_name][:type]
|
183
|
-
|
184
|
-
# Todo maybe better way here
|
185
|
-
interface = type_registry["I#{type_name}"]
|
186
|
-
|
187
|
-
local_type = interface[:type] if interface
|
188
|
-
local_resource_name = type_registry[type_name][:resource]
|
189
|
-
local_resource_class = Graphiti.resources.find { |r| r.name == local_resource_name }
|
190
|
-
|
191
|
-
local_types = [local_type]
|
192
|
-
if interface
|
193
|
-
local_types |= interface[:implementers]
|
194
|
-
end
|
195
|
-
|
196
|
-
local_types.each do |local|
|
197
|
-
local.field relationship.name,
|
198
|
-
type_registry[external_resource_config.type_name][:type], # todo need to define the type?
|
199
|
-
null: true
|
200
|
-
end
|
201
|
-
end
|
202
|
-
|
203
|
-
def apply_query(graphiti_schema, graphql_schema, entries)
|
204
|
-
query_type = generate_schema_query(graphql_schema, graphiti_schema, entries)
|
205
|
-
if self.class.federation?
|
206
|
-
apply_federation(graphiti_schema, schema)
|
207
|
-
end
|
75
|
+
def apply_query(entries)
|
76
|
+
query_type = generate_schema_query(entries)
|
77
|
+
Federation::SchemaDecorator.decorate(self) if self.class.federation?
|
208
78
|
|
209
79
|
# NB - don't call .query here of federation will break things
|
210
|
-
if
|
211
|
-
|
212
|
-
|
80
|
+
if schema.instance_variable_get(:@query_object)
|
81
|
+
schema.instance_variable_set(:@query_object, nil)
|
82
|
+
schema.instance_variable_set(:@federation_query_object, nil)
|
213
83
|
end
|
214
|
-
|
215
|
-
|
216
|
-
|
84
|
+
schema.orphan_types(orphans(schema))
|
85
|
+
schema.query(query_type)
|
86
|
+
schema.query # Actually fires the federation code
|
217
87
|
end
|
218
88
|
|
219
|
-
def generate_schema_query(
|
220
|
-
existing_query =
|
89
|
+
def generate_schema_query(entrypoint_resources = nil)
|
90
|
+
existing_query = schema.instance_variable_get(:@query)
|
91
|
+
existing_query ||= schema.send(:find_inherited_value, :query)
|
221
92
|
# NB - don't call graphql_schema.query here of federation will break things
|
222
93
|
query_class = Class.new(existing_query || self.class.base_object)
|
223
94
|
# NB MUST be Query or federation-ruby will break things
|
224
95
|
query_class.graphql_name "Query"
|
225
96
|
|
226
|
-
|
97
|
+
get_entrypoints(entrypoint_resources).each do |resource|
|
227
98
|
next if resource.remote?
|
228
99
|
generate_type(resource)
|
229
100
|
|
@@ -244,28 +115,46 @@ module GraphitiGraphQL
|
|
244
115
|
end
|
245
116
|
end
|
246
117
|
|
118
|
+
def schema_resource_for_query_field(name)
|
119
|
+
@query_fields[name.underscore.to_sym]
|
120
|
+
end
|
121
|
+
|
122
|
+
def query_field?(name)
|
123
|
+
@query_fields.include?(name.underscore.to_sym)
|
124
|
+
end
|
125
|
+
|
126
|
+
# We can't just constantize the name from the schema
|
127
|
+
# Because classes can be reopened and modified in tests (or elsewhere, in theory)
|
128
|
+
def resource_for_query_field(name)
|
129
|
+
schema_resource = @query_fields[name.underscore.to_sym]
|
130
|
+
Graphiti.resources.find { |r| r.name == schema_resource.name }
|
131
|
+
end
|
132
|
+
|
247
133
|
private
|
248
134
|
|
249
135
|
def add_index(query_class, resource)
|
250
|
-
|
136
|
+
field_name = resource.graphql_entrypoint.to_s.underscore.to_sym
|
137
|
+
field = query_class.field field_name,
|
251
138
|
[type_registry[resource.graphql_class_name][:type]],
|
252
139
|
"List #{resource.graphql_class_name(false).pluralize}",
|
253
140
|
null: false
|
141
|
+
@query_fields[field_name] = resource
|
254
142
|
define_arguments_for_sideload_field(field, resource)
|
255
143
|
end
|
256
144
|
|
257
145
|
def add_show(query_class, resource)
|
258
|
-
|
259
|
-
field = query_class.field
|
146
|
+
field_name = resource.graphql_entrypoint.to_s.underscore.singularize.to_sym
|
147
|
+
field = query_class.field field_name,
|
260
148
|
type_registry[resource.graphql_class_name][:type],
|
261
149
|
"Single #{resource.graphql_class_name(false).singularize}",
|
262
150
|
null: true
|
151
|
+
@query_fields[field_name] = resource
|
263
152
|
define_arguments_for_sideload_field field,
|
264
153
|
resource,
|
265
154
|
top_level_single: true
|
266
155
|
end
|
267
156
|
|
268
|
-
def
|
157
|
+
def get_entrypoints(manually_specified)
|
269
158
|
resources = graphiti_schema.resources
|
270
159
|
if manually_specified
|
271
160
|
resources = resources.select { |r|
|
@@ -309,13 +198,17 @@ module GraphitiGraphQL
|
|
309
198
|
if top_level_single
|
310
199
|
field.argument(:id, String, required: true)
|
311
200
|
else
|
312
|
-
|
313
|
-
|
201
|
+
unless resource.sorts.empty?
|
202
|
+
sort_type = generate_sort_type(resource)
|
203
|
+
field.argument :sort, [sort_type], required: false
|
204
|
+
end
|
314
205
|
field.argument :page, PageType, required: false
|
315
206
|
|
316
|
-
|
317
|
-
|
318
|
-
|
207
|
+
unless resource.filters.empty?
|
208
|
+
filter_type = generate_filter_type(field, resource)
|
209
|
+
required = resource.filters.any? { |name, config| !!config[:required] }
|
210
|
+
field.argument :filter, filter_type, required: required
|
211
|
+
end
|
319
212
|
end
|
320
213
|
end
|
321
214
|
|
@@ -413,8 +306,9 @@ module GraphitiGraphQL
|
|
413
306
|
klass = Module.new
|
414
307
|
klass.send(:include, self.class.base_interface)
|
415
308
|
klass.definition_methods do
|
309
|
+
# rubocop:disable Lint/NestedMethodDefinition(Standard)
|
416
310
|
def resolve_type(object, context)
|
417
|
-
|
311
|
+
GraphitiGraphQL.schemas.graphql.types[object[:__typename]]
|
418
312
|
end
|
419
313
|
end
|
420
314
|
else
|
@@ -426,10 +320,6 @@ module GraphitiGraphQL
|
|
426
320
|
implement(klass, type_registry[implements])
|
427
321
|
end
|
428
322
|
|
429
|
-
if self.class.federation?
|
430
|
-
klass.key fields: "id"
|
431
|
-
end
|
432
|
-
|
433
323
|
klass.field(:_type, String, null: false)
|
434
324
|
resource.all_attributes.each do |name, config|
|
435
325
|
if config[:readable]
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
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.2
|
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-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: graphiti
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.2.33
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.2.33
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: activesupport
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -227,6 +241,13 @@ files:
|
|
227
241
|
- lib/graphiti_graphql/engine.rb
|
228
242
|
- lib/graphiti_graphql/errors.rb
|
229
243
|
- lib/graphiti_graphql/federation.rb
|
244
|
+
- lib/graphiti_graphql/federation/apollo_federation_override.rb
|
245
|
+
- lib/graphiti_graphql/federation/federated_relationship.rb
|
246
|
+
- lib/graphiti_graphql/federation/federated_resource.rb
|
247
|
+
- lib/graphiti_graphql/federation/loaders/belongs_to.rb
|
248
|
+
- lib/graphiti_graphql/federation/loaders/has_many.rb
|
249
|
+
- lib/graphiti_graphql/federation/resource_dsl.rb
|
250
|
+
- lib/graphiti_graphql/federation/schema_decorator.rb
|
230
251
|
- lib/graphiti_graphql/graphiti_schema/resource.rb
|
231
252
|
- lib/graphiti_graphql/graphiti_schema/sideload.rb
|
232
253
|
- lib/graphiti_graphql/graphiti_schema/wrapper.rb
|