graphiti_graphql 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|