graphiti_graphql 0.1.0
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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +144 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/bundle +105 -0
- data/bin/byebug +29 -0
- data/bin/coderay +29 -0
- data/bin/console +14 -0
- data/bin/graphiti +29 -0
- data/bin/htmldiff +29 -0
- data/bin/ldiff +29 -0
- data/bin/pry +29 -0
- data/bin/rake +29 -0
- data/bin/rspec +29 -0
- data/bin/rubocop +29 -0
- data/bin/ruby-parse +29 -0
- data/bin/ruby-rewrite +29 -0
- data/bin/setup +8 -0
- data/bin/standardrb +29 -0
- data/config/routes.rb +6 -0
- data/graphiti_graphql.gemspec +45 -0
- data/lib/graphiti_graphql.rb +71 -0
- data/lib/graphiti_graphql/engine.rb +89 -0
- data/lib/graphiti_graphql/errors.rb +5 -0
- data/lib/graphiti_graphql/federation.rb +263 -0
- data/lib/graphiti_graphql/graphiti_schema/resource.rb +117 -0
- data/lib/graphiti_graphql/graphiti_schema/sideload.rb +56 -0
- data/lib/graphiti_graphql/graphiti_schema/wrapper.rb +36 -0
- data/lib/graphiti_graphql/runner.rb +313 -0
- data/lib/graphiti_graphql/schema.rb +538 -0
- data/lib/graphiti_graphql/util.rb +27 -0
- data/lib/graphiti_graphql/version.rb +3 -0
- metadata +260 -0
@@ -0,0 +1,313 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
class Runner
|
3
|
+
def execute(query_string, variables, schema)
|
4
|
+
query = GraphQL::Query.new(schema, query_string, variables: variables)
|
5
|
+
definition = query.document.definitions.first
|
6
|
+
selection = definition.selections.first
|
7
|
+
resource_class = find_entrypoint_resource_class(selection.name)
|
8
|
+
|
9
|
+
# TODO: instead, keep track of fields we add
|
10
|
+
Util.with_gql_context do
|
11
|
+
if resource_class
|
12
|
+
run_query(schema, resource_class, selection, query)
|
13
|
+
else
|
14
|
+
Graphiti.graphql_schema.schema.execute query_string,
|
15
|
+
variables: variables,
|
16
|
+
context: GraphitiGraphQL.get_context
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def run_query(schema, resource_class, selection, query)
|
24
|
+
if (errors = collect_errors(schema, query)).any?
|
25
|
+
{"errors" => errors.map(&:to_h)}
|
26
|
+
else
|
27
|
+
params = process_selection(selection, {}, query.variables.to_h)
|
28
|
+
json = resource_class.all(params).as_graphql
|
29
|
+
render(json, selection.name)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def render(json, selection_name)
|
34
|
+
payload = if find_one?(selection_name)
|
35
|
+
{selection_name.to_sym => json.values[0][0]}
|
36
|
+
else
|
37
|
+
json
|
38
|
+
end
|
39
|
+
{data: payload}
|
40
|
+
end
|
41
|
+
|
42
|
+
def find_one?(selection_name)
|
43
|
+
selection_name == selection_name.singularize
|
44
|
+
end
|
45
|
+
|
46
|
+
def collect_errors(schema, query)
|
47
|
+
query.analysis_errors = schema.analysis_engine
|
48
|
+
.analyze_query(query, query.analyzers || [])
|
49
|
+
query.validation_errors + query.analysis_errors + query.context.errors
|
50
|
+
end
|
51
|
+
|
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
|
+
def find_entrypoint_schema_resource(entrypoint)
|
59
|
+
graphiti_schema.resources.find(&matches_entrypoint?(entrypoint))
|
60
|
+
end
|
61
|
+
|
62
|
+
def matches_entrypoint?(entrypoint)
|
63
|
+
lambda do |resource|
|
64
|
+
resource.graphql_entrypoint.to_s.underscore == entrypoint.pluralize.underscore
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def introspection_query?(query)
|
69
|
+
query.document.definitions.first.selections.first.name == "__schema"
|
70
|
+
end
|
71
|
+
|
72
|
+
def find_resource_by_selection_name(name)
|
73
|
+
graphiti_schema.resources
|
74
|
+
.find { |r| r.type == name.pluralize.underscore }
|
75
|
+
end
|
76
|
+
|
77
|
+
def graphiti_schema
|
78
|
+
Graphiti.graphql_schema.graphiti_schema
|
79
|
+
end
|
80
|
+
|
81
|
+
def schema_resource_for_selection(selection, parent_resource)
|
82
|
+
if parent_resource
|
83
|
+
parent_resource.related_resource(selection.name.underscore.to_sym)
|
84
|
+
else
|
85
|
+
find_entrypoint_schema_resource(selection.name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def process_selection(
|
90
|
+
selection,
|
91
|
+
params,
|
92
|
+
variables_hash,
|
93
|
+
parent_resource = nil,
|
94
|
+
parent_name_chain = nil,
|
95
|
+
fragment_jsonapi_type: nil
|
96
|
+
)
|
97
|
+
selection_name = selection.name.underscore
|
98
|
+
|
99
|
+
pbt = false # polymorphic_belongs_to
|
100
|
+
if parent_resource
|
101
|
+
pbt = parent_resource.pbt?(selection_name.to_sym)
|
102
|
+
end
|
103
|
+
|
104
|
+
chained_name = nil
|
105
|
+
if fragment_jsonapi_type
|
106
|
+
selection_name = "on__#{fragment_jsonapi_type}--#{selection_name}"
|
107
|
+
end
|
108
|
+
|
109
|
+
if parent_resource
|
110
|
+
chained_name = selection_name
|
111
|
+
if parent_name_chain
|
112
|
+
chained_name = [parent_name_chain, selection_name].join(".")
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
if !pbt
|
117
|
+
resource = schema_resource_for_selection(selection, parent_resource)
|
118
|
+
gather_filters(params, selection, variables_hash, chained_name)
|
119
|
+
gather_sorts(params, selection, variables_hash, chained_name)
|
120
|
+
gather_pages(params, selection, variables_hash, chained_name)
|
121
|
+
end
|
122
|
+
|
123
|
+
params[:include] ||= []
|
124
|
+
params[:include] << chained_name if chained_name
|
125
|
+
|
126
|
+
fragments = selection.selections.select { |s|
|
127
|
+
s.is_a?(GraphQL::Language::Nodes::InlineFragment)
|
128
|
+
}
|
129
|
+
non_fragments = selection.selections - fragments
|
130
|
+
|
131
|
+
if pbt
|
132
|
+
# Only id/_type possible here
|
133
|
+
fields, extra_fields, sideload_selections = [], [], []
|
134
|
+
fields = non_fragments.map { |s| s.name.underscore }
|
135
|
+
# If fragments specified, these will get merged in later
|
136
|
+
if fragments.empty?
|
137
|
+
params[:fields][chained_name] = fields.join(",")
|
138
|
+
end
|
139
|
+
else
|
140
|
+
fields, extra_fields, sideload_selections =
|
141
|
+
gather_fields(non_fragments, resource, params, chained_name)
|
142
|
+
|
143
|
+
sideload_selections.each do |sideload_selection|
|
144
|
+
process_selection(sideload_selection, params, variables_hash, resource, chained_name)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
fragments.each do |fragment|
|
149
|
+
resource_name = Graphiti.graphql_schema.type_registry[fragment.type.name][:resource]
|
150
|
+
klass = graphiti_schema.resources.find { |r| r.name == resource_name }
|
151
|
+
_, _, fragment_sideload_selections = gather_fields fragment.selections,
|
152
|
+
klass,
|
153
|
+
params,
|
154
|
+
nil, # no chaining supported here
|
155
|
+
polymorphic_parent_data: [fields, extra_fields, sideload_selections]
|
156
|
+
|
157
|
+
fragment_sideload_selections.each do |sideload_selection|
|
158
|
+
fragment_jsonapi_type = klass.type
|
159
|
+
process_selection(sideload_selection, params, variables_hash, klass, chained_name, fragment_jsonapi_type: fragment_jsonapi_type)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
params
|
164
|
+
end
|
165
|
+
|
166
|
+
def gather_fields(
|
167
|
+
selections,
|
168
|
+
resource,
|
169
|
+
params,
|
170
|
+
chained_name,
|
171
|
+
polymorphic_parent_data: nil
|
172
|
+
)
|
173
|
+
fields, extra_fields, sideload_selections = [], [], []
|
174
|
+
selections.each do |sel|
|
175
|
+
selection_name = sel.name.underscore
|
176
|
+
sideload = resource.sideloads[selection_name.to_sym]
|
177
|
+
if sideload && !sideload.remote?
|
178
|
+
sideload_selections << sel
|
179
|
+
else
|
180
|
+
field_name = sel.name.underscore
|
181
|
+
if resource.extra_attributes[field_name.to_sym]
|
182
|
+
extra_fields << field_name
|
183
|
+
else
|
184
|
+
fields << field_name
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
if polymorphic_parent_data
|
190
|
+
fields |= polymorphic_parent_data[0]
|
191
|
+
extra_fields |= polymorphic_parent_data[1]
|
192
|
+
sideload_selections |= polymorphic_parent_data[2]
|
193
|
+
end
|
194
|
+
|
195
|
+
params[:fields] ||= {}
|
196
|
+
params[:extra_fields] ||= {}
|
197
|
+
if chained_name
|
198
|
+
field_param_name = chained_name
|
199
|
+
|
200
|
+
# If this is a polymorphic fragment subselection, the field is just the
|
201
|
+
# jsonapi type, for simplicity. TODO: Won't work if double-listing
|
202
|
+
last_chain = chained_name.split(".").last
|
203
|
+
if last_chain.starts_with?("on__")
|
204
|
+
field_param_name = last_chain.split("--")[1]
|
205
|
+
end
|
206
|
+
# Remove the special on__ flag from the chain, since not used for fields
|
207
|
+
field_param_name = field_param_name.gsub(/on__.*--/, "")
|
208
|
+
|
209
|
+
params[:fields][field_param_name.to_sym] = fields.join(",")
|
210
|
+
if extra_fields.present?
|
211
|
+
params[:extra_fields][field_param_name.to_sym] = extra_fields.join(",")
|
212
|
+
end
|
213
|
+
else
|
214
|
+
params[:fields][resource.type.to_sym] = fields.join(",")
|
215
|
+
if extra_fields.present?
|
216
|
+
params[:extra_fields][resource.type.to_sym] = extra_fields.join(",")
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
[fields, extra_fields, sideload_selections]
|
221
|
+
end
|
222
|
+
|
223
|
+
def gather_filters(params, selection, variable_hash, chained_name = nil)
|
224
|
+
filters = {}.tap do |f|
|
225
|
+
arg = selection.arguments.find { |arg| arg.name == "filter" }
|
226
|
+
arg ||= selection.arguments.find { |arg| arg.name == "id" }
|
227
|
+
|
228
|
+
if arg
|
229
|
+
if arg.name == "filter"
|
230
|
+
arg.children[0].arguments.each do |attr_arg|
|
231
|
+
field_name = attr_arg.name.underscore
|
232
|
+
filter_param_name = [chained_name, field_name].compact.join(".")
|
233
|
+
|
234
|
+
attr_arg.value.arguments.each do |operator_arg|
|
235
|
+
value = operator_arg.value
|
236
|
+
if value.respond_to?(:name) # is a variable
|
237
|
+
value = variable_hash[operator_arg.value.name]
|
238
|
+
end
|
239
|
+
f[filter_param_name] = {operator_arg.name.underscore => value}
|
240
|
+
end
|
241
|
+
end
|
242
|
+
else
|
243
|
+
value = arg.value
|
244
|
+
if value.respond_to?(:name) # is a variable
|
245
|
+
value = variable_hash[arg.value.name]
|
246
|
+
end
|
247
|
+
f[:id] = {eq: value}
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
if filters
|
253
|
+
params[:filter] ||= {}
|
254
|
+
params[:filter].merge!(filters)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def gather_sorts(params, selection, variable_hash, chained_name = nil)
|
259
|
+
sorts = [].tap do |s|
|
260
|
+
selection.arguments.each do |arg|
|
261
|
+
if arg.name == "sort"
|
262
|
+
value = if arg.value.respond_to?(:name) # is a variable
|
263
|
+
variable_hash[arg.value.name].map(&:to_h)
|
264
|
+
else
|
265
|
+
arg.value.map(&:to_h)
|
266
|
+
end
|
267
|
+
jsonapi_values = value.map { |v|
|
268
|
+
att = (v[:att] || v["att"]).underscore
|
269
|
+
att = [chained_name, att].compact.join(".")
|
270
|
+
if v["dir"] == "desc"
|
271
|
+
att = "-#{att}"
|
272
|
+
end
|
273
|
+
att
|
274
|
+
}
|
275
|
+
|
276
|
+
s << jsonapi_values.join(",")
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
if sorts.present?
|
282
|
+
params[:sort] = [params[:sort], sorts].compact.join(",")
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def gather_pages(params, selection, variable_hash, chained_name = nil)
|
287
|
+
pages = {}.tap do |p|
|
288
|
+
selection.arguments.each do |arg|
|
289
|
+
if arg.name == "page"
|
290
|
+
value = if arg.value.respond_to?(:name) # is a variable
|
291
|
+
variable_hash[arg.value.name].to_h
|
292
|
+
else
|
293
|
+
arg.value.to_h
|
294
|
+
end
|
295
|
+
|
296
|
+
if chained_name
|
297
|
+
value.each_pair do |k, v|
|
298
|
+
p["#{chained_name}.#{k}"] = v
|
299
|
+
end
|
300
|
+
else
|
301
|
+
p.merge!(value)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
if pages.present?
|
308
|
+
params[:page] ||= {}
|
309
|
+
params[:page].merge!(pages)
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
end
|
@@ -0,0 +1,538 @@
|
|
1
|
+
module GraphitiGraphQL
|
2
|
+
class Schema
|
3
|
+
GQL_TYPE_MAP = {
|
4
|
+
integer_id: String,
|
5
|
+
string: String,
|
6
|
+
integer: Integer,
|
7
|
+
float: Float,
|
8
|
+
boolean: GraphQL::Schema::Member::GraphQLTypeNames::Boolean,
|
9
|
+
date: GraphQL::Types::ISO8601Date,
|
10
|
+
datetime: GraphQL::Types::ISO8601DateTime,
|
11
|
+
hash: GraphQL::Types::JSON,
|
12
|
+
array: [GraphQL::Types::JSON],
|
13
|
+
array_of_strings: [String],
|
14
|
+
array_of_integers: [Integer],
|
15
|
+
array_of_floats: [Float],
|
16
|
+
array_of_dates: [GraphQL::Types::ISO8601Date],
|
17
|
+
array_of_datetimes: [GraphQL::Types::ISO8601DateTime]
|
18
|
+
}
|
19
|
+
|
20
|
+
class BaseField < GraphQL::Schema::Field
|
21
|
+
end
|
22
|
+
|
23
|
+
class BaseObject < GraphQL::Schema::Object
|
24
|
+
end
|
25
|
+
|
26
|
+
module BaseInterface
|
27
|
+
include GraphQL::Schema::Interface
|
28
|
+
end
|
29
|
+
|
30
|
+
class << self
|
31
|
+
attr_accessor :entrypoints, :federation
|
32
|
+
attr_writer :base_field, :base_object, :base_interface
|
33
|
+
end
|
34
|
+
|
35
|
+
attr_accessor :type_registry, :schema, :graphiti_schema
|
36
|
+
|
37
|
+
def self.federation?
|
38
|
+
!!@federation
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.base_field
|
42
|
+
@base_field || BaseField
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.base_object
|
46
|
+
@base_object || BaseObject
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.base_interface
|
50
|
+
@base_interface || BaseInterface
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.generate(entrypoint_resources = nil)
|
54
|
+
instance = new
|
55
|
+
schema = Class.new(::GraphitiGraphQL.schema_class || GraphQL::Schema)
|
56
|
+
|
57
|
+
if federation?
|
58
|
+
schema.send(:include, ApolloFederation::Schema)
|
59
|
+
end
|
60
|
+
|
61
|
+
graphiti_schema = GraphitiGraphQL::GraphitiSchema::Wrapper
|
62
|
+
.new(Graphiti::Schema.generate)
|
63
|
+
# TODO: if we avoid this on federation, or remove altogether
|
64
|
+
# Make sure we don't blow up
|
65
|
+
# graphiti_schema.merge_remotes!
|
66
|
+
|
67
|
+
entries = entrypoint_resources || entrypoints
|
68
|
+
instance.apply_query(graphiti_schema, schema, entries)
|
69
|
+
|
70
|
+
# NB if we add mutation support, make sure this is applied after
|
71
|
+
if federation?
|
72
|
+
schema.use GraphQL::Batch
|
73
|
+
end
|
74
|
+
instance.schema = schema
|
75
|
+
instance.graphiti_schema = graphiti_schema
|
76
|
+
instance
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.resource_class_for_type(type_name)
|
80
|
+
const_get "#{type_name}Resource".gsub("__", "")
|
81
|
+
end
|
82
|
+
|
83
|
+
def initialize
|
84
|
+
@type_registry = {}
|
85
|
+
end
|
86
|
+
|
87
|
+
# TODO put this in a Federation::Schema module
|
88
|
+
# Maybe even the External classes themselves?
|
89
|
+
# TODO assign/assign_each
|
90
|
+
def apply_federation(graphiti_schema, graphql_schema)
|
91
|
+
type_registry.each_pair do |name, config|
|
92
|
+
if config[:resource]
|
93
|
+
local_type = config[:type]
|
94
|
+
local_resource = Graphiti.resources
|
95
|
+
.find { |r| r.name == config[:resource] }
|
96
|
+
# TODO: maybe turn off the graphiti debug for these?
|
97
|
+
local_type.define_singleton_method :resolve_reference do |reference, context, lookahead|
|
98
|
+
Federation::BelongsToLoader
|
99
|
+
.for(local_resource, lookahead.selections.map(&:name))
|
100
|
+
.load(reference[:id])
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# NB: test already registered bc 2 things have same relationship
|
106
|
+
GraphitiGraphQL::Federation.external_resources.each_pair do |klass_name, config|
|
107
|
+
pre_registered = !!type_registry[klass_name]
|
108
|
+
external_klass = if pre_registered
|
109
|
+
type_registry[klass_name][:type]
|
110
|
+
else
|
111
|
+
external_klass = Class.new(self.class.base_object)
|
112
|
+
external_klass.graphql_name klass_name
|
113
|
+
external_klass
|
114
|
+
end
|
115
|
+
|
116
|
+
unless pre_registered
|
117
|
+
external_klass.key(fields: "id")
|
118
|
+
external_klass.extend_type
|
119
|
+
external_klass.field :id, String, null: false, external: true
|
120
|
+
external_klass.class_eval do
|
121
|
+
def self.resolve_reference(reference, _context, _lookup)
|
122
|
+
reference
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
unless pre_registered
|
128
|
+
# NB must be registered before processing rels
|
129
|
+
type_registry[klass_name] = { type: external_klass }
|
130
|
+
end
|
131
|
+
|
132
|
+
# TODO: only do it if field not already defined
|
133
|
+
config.relationships.each_pair do |name, relationship|
|
134
|
+
if relationship.has_many?
|
135
|
+
define_federated_has_many(graphiti_schema, external_klass, relationship)
|
136
|
+
elsif relationship.belongs_to?
|
137
|
+
define_federated_belongs_to(config, relationship)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# TODO: refactor to not constantly pass schemas around
|
144
|
+
def define_federated_has_many(graphiti_schema, external_klass, relationship)
|
145
|
+
local_name = GraphitiGraphQL::GraphitiSchema::Resource
|
146
|
+
.gql_name(relationship.local_resource_class.name)
|
147
|
+
local_type = type_registry[local_name][:type]
|
148
|
+
local_resource_name = type_registry[local_name][:resource]
|
149
|
+
local_resource = Graphiti.resources.find { |r| r.name == local_resource_name }
|
150
|
+
|
151
|
+
local_interface = type_registry["I#{local_name}"]
|
152
|
+
best_type = local_interface ? local_interface[:type] : local_type
|
153
|
+
|
154
|
+
field = external_klass.field relationship.name,
|
155
|
+
[best_type],
|
156
|
+
null: false,
|
157
|
+
extras: [:lookahead]
|
158
|
+
|
159
|
+
define_arguments_for_sideload_field(field, graphiti_schema.get_resource(local_resource_name))
|
160
|
+
external_klass.define_method relationship.name do |lookahead:, **arguments|
|
161
|
+
# TODO test params...do version of sort with array/symbol keys and plain string
|
162
|
+
params = arguments.as_json
|
163
|
+
.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
164
|
+
selections = lookahead.selections.map(&:name)
|
165
|
+
selections << relationship.foreign_key
|
166
|
+
selections << :_type # polymorphism
|
167
|
+
params[:fields] = { local_resource.type => selections.join(",") }
|
168
|
+
|
169
|
+
if (sort = Util.parse_sort(params[:sort]))
|
170
|
+
params[:sort] = sort
|
171
|
+
end
|
172
|
+
|
173
|
+
Federation::HasManyLoader
|
174
|
+
.for(local_resource, params, relationship.foreign_key)
|
175
|
+
.load(object[:id])
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def define_federated_belongs_to(external_resource_config, relationship)
|
180
|
+
type_name = GraphitiSchema::Resource.gql_name(relationship.local_resource_class.name)
|
181
|
+
local_type = type_registry[type_name][:type]
|
182
|
+
|
183
|
+
# Todo maybe better way here
|
184
|
+
interface = type_registry["I#{type_name}"]
|
185
|
+
|
186
|
+
local_type = interface[:type] if interface
|
187
|
+
local_resource_name = type_registry[type_name][:resource]
|
188
|
+
local_resource_class = Graphiti.resources.find { |r| r.name == local_resource_name }
|
189
|
+
|
190
|
+
local_types = [local_type]
|
191
|
+
if interface
|
192
|
+
local_types |= interface[:implementers]
|
193
|
+
end
|
194
|
+
|
195
|
+
local_types.each do |local|
|
196
|
+
local.field relationship.name,
|
197
|
+
type_registry[external_resource_config.type_name][:type], # todo need to define the type?
|
198
|
+
null: true
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def apply_query(graphiti_schema, graphql_schema, entries)
|
203
|
+
query_type = generate_schema_query(graphql_schema, graphiti_schema, entries)
|
204
|
+
if self.class.federation?
|
205
|
+
apply_federation(graphiti_schema, schema)
|
206
|
+
end
|
207
|
+
|
208
|
+
# NB - don't call .query here of federation will break things
|
209
|
+
if graphql_schema.instance_variable_get(:@query_object)
|
210
|
+
graphql_schema.instance_variable_set(:@query_object, nil)
|
211
|
+
graphql_schema.instance_variable_set(:@federation_query_object, nil)
|
212
|
+
end
|
213
|
+
graphql_schema.orphan_types(orphans(graphql_schema))
|
214
|
+
graphql_schema.query(query_type)
|
215
|
+
graphql_schema.query # Actually fires the federation code
|
216
|
+
end
|
217
|
+
|
218
|
+
def generate_schema_query(graphql_schema, graphiti_schema, entrypoint_resources = nil)
|
219
|
+
existing_query = graphql_schema.instance_variable_get(:@query) || graphql_schema.send(:find_inherited_value, :query)
|
220
|
+
# NB - don't call graphql_schema.query here of federation will break things
|
221
|
+
query_class = Class.new(existing_query || self.class.base_object)
|
222
|
+
# NB MUST be Query or federation-ruby will break things
|
223
|
+
query_class.graphql_name "Query"
|
224
|
+
|
225
|
+
entrypoints(graphiti_schema, entrypoint_resources).each do |resource|
|
226
|
+
next if resource.remote?
|
227
|
+
generate_type(resource)
|
228
|
+
|
229
|
+
add_index(query_class, resource)
|
230
|
+
add_show(query_class, resource)
|
231
|
+
end
|
232
|
+
query_class
|
233
|
+
end
|
234
|
+
|
235
|
+
def orphans(graphql_schema)
|
236
|
+
[].tap do |orphans|
|
237
|
+
type_registry.keys.each do |type_name|
|
238
|
+
unless graphql_schema.types.has_key?(type_name)
|
239
|
+
klass = type_registry[type_name][:type]
|
240
|
+
orphans << klass if klass.is_a?(Class)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def add_index(query_class, resource)
|
249
|
+
field = query_class.field resource.graphql_entrypoint,
|
250
|
+
[type_registry[resource.graphql_class_name][:type]],
|
251
|
+
"List #{resource.graphql_class_name(false).pluralize}",
|
252
|
+
null: false
|
253
|
+
define_arguments_for_sideload_field(field, resource)
|
254
|
+
end
|
255
|
+
|
256
|
+
def add_show(query_class, resource)
|
257
|
+
entrypoint = resource.graphql_entrypoint.to_s.singularize.to_sym
|
258
|
+
field = query_class.field entrypoint,
|
259
|
+
type_registry[resource.graphql_class_name][:type],
|
260
|
+
"Single #{resource.graphql_class_name(false).singularize}",
|
261
|
+
null: true
|
262
|
+
define_arguments_for_sideload_field field,
|
263
|
+
resource,
|
264
|
+
top_level_single: true
|
265
|
+
end
|
266
|
+
|
267
|
+
def entrypoints(graphiti_schema, manually_specified)
|
268
|
+
resources = graphiti_schema.resources
|
269
|
+
if manually_specified
|
270
|
+
resources = resources.select { |r|
|
271
|
+
manually_specified.map(&:name).include?(r.name)
|
272
|
+
}
|
273
|
+
end
|
274
|
+
resources
|
275
|
+
end
|
276
|
+
|
277
|
+
def generate_sort_att_type_for(resource)
|
278
|
+
type_name = "#{resource.graphql_class_name(false)}SortAtt"
|
279
|
+
if (registered = type_registry[type_name])
|
280
|
+
return registered[:type]
|
281
|
+
end
|
282
|
+
klass = Class.new(GraphQL::Schema::Enum) {
|
283
|
+
graphql_name(type_name)
|
284
|
+
}
|
285
|
+
resource.sorts.each_pair do |name, config|
|
286
|
+
klass.value name.to_s.camelize(:lower), "Sort by #{name}"
|
287
|
+
end
|
288
|
+
register(type_name, klass, resource)
|
289
|
+
klass
|
290
|
+
end
|
291
|
+
|
292
|
+
def generate_sort_type(resource)
|
293
|
+
type_name = "#{resource.graphql_class_name(false)}Sort"
|
294
|
+
if (registered = type_registry[type_name])
|
295
|
+
return registered[:type]
|
296
|
+
end
|
297
|
+
att_type = generate_sort_att_type_for(resource)
|
298
|
+
klass = Class.new(GraphQL::Schema::InputObject) {
|
299
|
+
graphql_name type_name
|
300
|
+
argument :att, att_type, required: true
|
301
|
+
argument :dir, SortDirType, required: true
|
302
|
+
}
|
303
|
+
register(type_name, klass)
|
304
|
+
klass
|
305
|
+
end
|
306
|
+
|
307
|
+
def define_arguments_for_sideload_field(field, resource, top_level_single: false)
|
308
|
+
if top_level_single
|
309
|
+
field.argument(:id, String, required: true)
|
310
|
+
else
|
311
|
+
sort_type = generate_sort_type(resource)
|
312
|
+
field.argument :sort, [sort_type], required: false
|
313
|
+
field.argument :page, PageType, required: false
|
314
|
+
|
315
|
+
filter_type = generate_filter_type(field, resource)
|
316
|
+
required = resource.filters.any? { |name, config| !!config[:required] }
|
317
|
+
field.argument :filter, filter_type, required: required
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
def generate_filter_type(field, resource)
|
322
|
+
type_name = "#{resource.graphql_class_name(false)}Filter"
|
323
|
+
if (registered = type_registry[type_name])
|
324
|
+
return registered[:type]
|
325
|
+
end
|
326
|
+
klass = Class.new(GraphQL::Schema::InputObject)
|
327
|
+
klass.graphql_name type_name
|
328
|
+
resource.filters.each_pair do |name, config|
|
329
|
+
attr_type = generate_filter_attribute_type(type_name, name, config)
|
330
|
+
klass.argument name.to_s.camelize(:lower),
|
331
|
+
attr_type,
|
332
|
+
required: !!config[:required]
|
333
|
+
end
|
334
|
+
register(type_name, klass)
|
335
|
+
klass
|
336
|
+
end
|
337
|
+
|
338
|
+
# TODO guarded operators or otherwise whatever eq => nil is
|
339
|
+
def generate_filter_attribute_type(type_name, filter_name, filter_config)
|
340
|
+
klass = Class.new(GraphQL::Schema::InputObject)
|
341
|
+
klass.graphql_name "#{type_name}Filter#{filter_name.to_s.camelize(:lower)}"
|
342
|
+
filter_config[:operators].each do |operator|
|
343
|
+
canonical_graphiti_type = Graphiti::Types
|
344
|
+
.name_for(filter_config[:type])
|
345
|
+
type = GQL_TYPE_MAP[canonical_graphiti_type]
|
346
|
+
required = !!filter_config[:required] && operator == "eq"
|
347
|
+
klass.argument operator, type, required: required
|
348
|
+
end
|
349
|
+
klass
|
350
|
+
end
|
351
|
+
|
352
|
+
def generate_resource_for_sideload(sideload)
|
353
|
+
if sideload.type == :polymorphic_belongs_to
|
354
|
+
unless registered?(sideload.parent_resource)
|
355
|
+
generate_type(sideload.parent_resource)
|
356
|
+
end
|
357
|
+
else
|
358
|
+
unless registered?(sideload.resource)
|
359
|
+
generate_type(sideload.resource)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
def add_relationships_to_type_class(type_class, resource, processed = [])
|
365
|
+
type_name = resource.graphql_class_name(false)
|
366
|
+
return if processed.include?(type_name)
|
367
|
+
|
368
|
+
resource.sideloads.each_pair do |name, sideload|
|
369
|
+
next if sideload.remote?
|
370
|
+
generate_resource_for_sideload(sideload)
|
371
|
+
|
372
|
+
gql_type = if sideload.type == :polymorphic_belongs_to
|
373
|
+
interface_for_pbt(resource, sideload)
|
374
|
+
else
|
375
|
+
type_registry[sideload.graphql_class_name][:type]
|
376
|
+
end
|
377
|
+
|
378
|
+
gql_field_type = sideload.to_many? ? [gql_type] : gql_type
|
379
|
+
field_name = name.to_s.camelize(:lower)
|
380
|
+
unless type_class.fields[field_name]
|
381
|
+
field = type_class.field field_name.to_sym,
|
382
|
+
gql_field_type,
|
383
|
+
null: !sideload.to_many?
|
384
|
+
|
385
|
+
# No sort/filter/paginate on belongs_to
|
386
|
+
# unless sideload.type.to_s.include?('belongs_to')
|
387
|
+
unless sideload.type == :polymorphic_belongs_to
|
388
|
+
define_arguments_for_sideload_field(field, sideload.resource)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
processed << type_name
|
393
|
+
|
394
|
+
# For PBT, the relationships are only possible on fragments
|
395
|
+
unless sideload.type == :polymorphic_belongs_to
|
396
|
+
add_relationships_to_type_class(gql_type, sideload.resource, processed)
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
400
|
+
|
401
|
+
def generate_type(resource, implements = nil)
|
402
|
+
return if resource.remote?
|
403
|
+
return if registered?(resource)
|
404
|
+
type_name = resource.graphql_class_name(false)
|
405
|
+
|
406
|
+
# Define the interface
|
407
|
+
klass = nil
|
408
|
+
poly_parent = resource.polymorphic? && !implements
|
409
|
+
|
410
|
+
if poly_parent
|
411
|
+
type_name = "I#{type_name}"
|
412
|
+
klass = Module.new
|
413
|
+
klass.send(:include, self.class.base_interface)
|
414
|
+
klass.definition_methods do
|
415
|
+
def resolve_type(object, context)
|
416
|
+
Graphiti.graphql_schema.schema.types[object[:__typename]]
|
417
|
+
end
|
418
|
+
end
|
419
|
+
else
|
420
|
+
klass = Class.new(self.class.base_object)
|
421
|
+
end
|
422
|
+
klass.graphql_name type_name
|
423
|
+
|
424
|
+
if implements
|
425
|
+
implement(klass, type_registry[implements])
|
426
|
+
end
|
427
|
+
|
428
|
+
if self.class.federation?
|
429
|
+
klass.key fields: "id"
|
430
|
+
end
|
431
|
+
|
432
|
+
klass.field(:_type, String, null: false)
|
433
|
+
resource.all_attributes.each do |name, config|
|
434
|
+
if config[:readable]
|
435
|
+
canonical_graphiti_type = Graphiti::Types.name_for(config[:type])
|
436
|
+
gql_type = GQL_TYPE_MAP[canonical_graphiti_type.to_sym]
|
437
|
+
gql_type = String if name == :id
|
438
|
+
# Todo document we don't have the concept, but can build it
|
439
|
+
is_nullable = !(name == :id)
|
440
|
+
klass.field(name, gql_type, null: is_nullable)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
register(type_name, klass, resource, poly_parent)
|
445
|
+
|
446
|
+
resource.sideloads.each_pair do |name, sideload|
|
447
|
+
if sideload.type == :polymorphic_belongs_to
|
448
|
+
sideload.child_resources.each do |child_resource|
|
449
|
+
unless registered?(child_resource)
|
450
|
+
generate_type(child_resource)
|
451
|
+
end
|
452
|
+
end
|
453
|
+
else
|
454
|
+
unless registered?(sideload.resource)
|
455
|
+
generate_type(sideload.resource)
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
# Define the actual class that implements the interface
|
461
|
+
if poly_parent
|
462
|
+
canonical_name = resource.graphql_class_name(false)
|
463
|
+
klass = Class.new(self.class.base_object)
|
464
|
+
implement(klass, type_registry[type_name])
|
465
|
+
klass.graphql_name canonical_name
|
466
|
+
register(canonical_name, klass, resource)
|
467
|
+
end
|
468
|
+
|
469
|
+
if poly_parent
|
470
|
+
resource.children.each do |child|
|
471
|
+
if registered?(child)
|
472
|
+
child_klass = type_registry[child.graphql_class_name][:type]
|
473
|
+
child_klass.implements(type_registry[type_name][:type])
|
474
|
+
else
|
475
|
+
generate_type(child, type_name)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
add_relationships_to_type_class(klass, resource)
|
481
|
+
|
482
|
+
klass
|
483
|
+
end
|
484
|
+
|
485
|
+
def registered?(resource)
|
486
|
+
name = resource.graphql_class_name(false)
|
487
|
+
!!type_registry[name]
|
488
|
+
end
|
489
|
+
|
490
|
+
def register(name, klass, resource = nil, interface = nil)
|
491
|
+
value = {type: klass}
|
492
|
+
value[:resource] = resource.name if resource
|
493
|
+
value[:jsonapi_type] = resource.type if resource
|
494
|
+
if interface
|
495
|
+
value[:interface] = true
|
496
|
+
value[:implementers] = []
|
497
|
+
end
|
498
|
+
type_registry[name] = value
|
499
|
+
end
|
500
|
+
|
501
|
+
def implement(type_class, interface_config)
|
502
|
+
type_class.implements(interface_config[:type])
|
503
|
+
interface_config[:implementers] << type_class
|
504
|
+
end
|
505
|
+
|
506
|
+
# Define interface for polymorphic_belongs_to sideload
|
507
|
+
# After defining, ensure child resources implement the interface
|
508
|
+
def interface_for_pbt(resource, sideload)
|
509
|
+
type_name = "#{resource.graphql_class_name}__#{sideload.name}"
|
510
|
+
interface = type_registry[type_name]
|
511
|
+
if !interface
|
512
|
+
klass = Module.new
|
513
|
+
klass.send :include, self.class.base_interface
|
514
|
+
klass.field :id, String, null: false
|
515
|
+
klass.field :_type, String, null: false
|
516
|
+
klass.graphql_name type_name
|
517
|
+
sideload.child_resources.each do |r|
|
518
|
+
type_registry[r.graphql_class_name][:type].implements(klass)
|
519
|
+
end
|
520
|
+
register(type_name, klass)
|
521
|
+
interface = klass
|
522
|
+
end
|
523
|
+
interface
|
524
|
+
end
|
525
|
+
|
526
|
+
class PageType < GraphQL::Schema::InputObject
|
527
|
+
graphql_name "Page"
|
528
|
+
argument :size, Int, required: false
|
529
|
+
argument :number, Int, required: false
|
530
|
+
end
|
531
|
+
|
532
|
+
class SortDirType < GraphQL::Schema::Enum
|
533
|
+
graphql_name "SortDir"
|
534
|
+
value "asc", "Ascending"
|
535
|
+
value "desc", "Descending"
|
536
|
+
end
|
537
|
+
end
|
538
|
+
end
|