graphiti_graphql 0.1.0

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